28 Commits
Author SHA1 Message Date
legonzaur 0fbe38115f update hostname 2023-09-03 13:23:54 +02:00
legonzaur b827957d8a update og_image 2023-07-07 13:59:37 +02:00
legonzaur 5ca3c56564 fix locale on piechart not showing sometimes 2023-06-22 18:43:28 +02:00
legonzaur 9a5ba1178d merge gamemode and weapon into one reusable component 2023-06-22 18:38:51 +02:00
legonzaur 50adf2d9f0 fix chart width 2023-06-22 18:24:50 +02:00
legonzaur 979a65f443 add other gamemode localization 2023-06-22 10:38:11 +02:00
legonzaur 77116443ec update og image 2023-06-21 13:31:06 +02:00
legonzaur 8d1167fc64 add package lock 2023-06-21 13:24:27 +02:00
legonzaur 89bf501a2f remove duplicate eslint rule 2023-06-21 13:20:47 +02:00
legonzaur 6f85e223d0 Merge branch 'master' into multiselection 2023-06-21 13:18:30 +02:00
legonzaur ac31b7fb7a add gamemode filter and multiselection 2023-06-21 13:10:37 +02:00
legonzaur 99272676cb upgrade plugins 2023-06-21 12:08:30 +02:00
legonzaur 7d38564107 cache into map 2023-06-20 20:46:14 +02:00
legonzaur 93063e125c Revert "Add multiselection"
This reverts commit a6e75006e6.
2023-06-20 12:52:01 +02:00
legonzaur 00f6016311 update og_image 2023-06-20 12:36:30 +02:00
legonzaur a6e75006e6 Add multiselection 2023-06-20 12:24:33 +02:00
legonzaur 8bdfc2586a Merge branch 'master' of https://github.com/ToneAPI/ToneAPI_webclient 2023-06-01 13:06:50 +02:00
legonzaur b26a971de1 add back server realtime updates 2023-06-01 13:06:48 +02:00
Legonzaur 9b7bf03e1c Update node.js.yml 2023-06-01 12:50:19 +02:00
Legonzaur 2f272e48e6 Update node.js.yml 2023-06-01 12:48:56 +02:00
Legonzaur 6d1c0d0d22 Update node.js.yml 2023-06-01 12:41:19 +02:00
Legonzaur f35196ab4a Create node.js.yml 2023-06-01 12:39:53 +02:00
legonzaur 51fbc60036 re-request api on filter cha,nge 2023-06-01 12:30:43 +02:00
legonzaur 76b56c62ce only update the current view 2023-06-01 12:11:25 +02:00
legonzaur 10cf90bcf3 Fix server image for chromium browsers 2023-06-01 11:01:58 +02:00
legonzaur 45cf95aa22 remove dist directory after publish 2023-06-01 10:59:52 +02:00
legonzaur c17d9c4a73 Revert "'publish'"
This reverts commit 288920ded8.
2023-06-01 10:58:03 +02:00
legonzaur 288920ded8 'publish' 2023-06-01 10:36:49 +02:00
19 changed files with 2117 additions and 3189 deletions
+6
View File
@@ -3,3 +3,9 @@ indent_style = space
indent_size = 2 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true
insert_final_newline = true insert_final_newline = true
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
+7 -1
View File
@@ -1,18 +1,24 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true node: true
}, },
extends: [ extends: [
'plugin:vue/vue3-essential', 'plugin:vue/vue3-essential',
'@vue/standard', '@vue/standard',
'@vue/typescript/recommended' '@vue/typescript/recommended'
], ],
parserOptions: { parserOptions: {
ecmaVersion: 2020 ecmaVersion: 2020
}, },
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'@typescript-eslint/indent': ['error', 2],
camelcase: 0
} }
} }
+39
View File
@@ -0,0 +1,39 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
name: Node.js CI
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write
pages: write
strategy:
matrix:
node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci --force
- run: npm run build --if-present
- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@v1.0.8
with:
path: dist
- name: Deploy GitHub Pages site
uses: actions/deploy-pages@v2.0.1
+1589 -2706
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -26,9 +26,9 @@
"@typescript-eslint/eslint-plugin": "^5.4.0", "@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.0", "@typescript-eslint/parser": "^5.4.0",
"@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.8",
"@vue/cli-plugin-router": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.0", "@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.0", "@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0", "@vue/cli-service": "~5.0.0",
"@vue/eslint-config-standard": "^6.1.0", "@vue/eslint-config-standard": "^6.1.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 269 KiB

+2
View File
@@ -2,3 +2,5 @@ call npm run build
call git add dist -f call git add dist -f
call git commit -m 'publish' call git commit -m 'publish'
call git subtree push --prefix dist origin gh-pages call git subtree push --prefix dist origin gh-pages
call git reset HEAD~1
call git rm dist -r
-65
View File
@@ -1,65 +0,0 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-typescript" target="_blank" rel="noopener">typescript</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script lang="ts">
import { Options, Vue } from 'vue-class-component'
@Options({
props: {
msg: String
}
})
export default class HelloWorld extends Vue {
msg!: string
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
+3 -3
View File
@@ -66,11 +66,11 @@ export default defineComponent({
} else if (mustScrollDown) { } else if (mustScrollDown) {
this.vIndex = Math.min(scrollIndex, this.list.length - this.toLoad) this.vIndex = Math.min(scrollIndex, this.list.length - this.toLoad)
} }
if (mustScrollDown || mustScrollUp) { // if (mustScrollDown || mustScrollUp) {
const msg = (mustScrollDown ? 'down ' : '') + (mustScrollUp ? 'up ' : '') // const msg = (mustScrollDown ? 'down ' : '') + (mustScrollUp ? 'up ' : '')
// console.log(this.toLoad) // console.log(this.toLoad)
// console.log((this.list.length * this.$props.rowHeight) - (this.vIndex * this.$props.rowHeight) - (this.visibleCount * this.$props.rowHeight)) // console.log((this.list.length * this.$props.rowHeight) - (this.vIndex * this.$props.rowHeight) - (this.visibleCount * this.$props.rowHeight))
} // }
} }
} }
@@ -1,47 +1,58 @@
<template> <template>
<div class="weaponChart" ref="container"> <div class="chart" ref="container">
<LoadingBar v-if="progress !== 1" :value="progress"></LoadingBar> <LoadingBar v-if="progress !== 1" :value="progress"></LoadingBar>
<Doughnut :data="chart" :options="chartOptions" v-if="progress === 1" /> <Doughnut :data="chart" :options="chartOptions" v-if="progress === 1" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js' import { Chart as ChartJS, ArcElement, Tooltip, Legend, ChartData, ChartOptions } from 'chart.js'
import dataLabel from 'chartjs-plugin-datalabels' import dataLabel from 'chartjs-plugin-datalabels'
import { Weapon, Filters, useKillStore } from '@/stores/kill' import { Weapon, Filter, useKillStore } from '@/stores/kill'
import { Doughnut } from 'vue-chartjs' import { Doughnut } from 'vue-chartjs'
import { defineComponent, PropType, Ref } from 'vue' import { defineComponent, PropType, Ref, triggerRef, unref } from 'vue'
import weapons from '../stores/weapons.json'
import LoadingBar from './LoadingBar.vue' import LoadingBar from './LoadingBar.vue'
ChartJS.register(ArcElement, Tooltip, Legend, dataLabel) ChartJS.register(ArcElement, Tooltip, Legend, dataLabel)
export default defineComponent({ export default defineComponent({
name: 'WeaponChart', name: 'PieChart',
props: { props: {
filters: Object as PropType<Filters>, filters: Object as PropType<Filter>,
playerHighlighted: String playerHighlighted: String,
type: Object as PropType<'server' | 'player' | 'weapon' | 'map' | 'gamemode'>
}, },
emits: ['highlightPlayer'], emits: ['highlightPlayer'],
components: { components: {
Doughnut, LoadingBar Doughnut, LoadingBar
}, },
created () {
// eslint-disable-next-line no-extra-bind
import(`../stores/${this.type}s.json`).then((e => { this.locale = e }).bind(this))
},
mounted () { mounted () {
this.refreshColors++ this.refreshColors++
}, },
data: () => { data: () => {
return { return {
store: useKillStore(), store: useKillStore(),
refreshColors: 0 refreshColors: 0,
locale: {} as {[key:string]:string}
} }
}, },
computed: { computed: {
progress () { progress () {
const { weapon: _, ...withoutWeapon } = this.filters || {} const filter = new Filter(this.filters)
const data = this.store.getWeaponList(withoutWeapon)?.value if (this.type === undefined) return 0
if (!data) return this.store.fetchWeapons(withoutWeapon).value.progress if (!(['server', 'player', 'weapon', 'map', 'gamemode'].includes(this.type))) return 0
return data.progress if (Object.keys(this.locale).length === 0) return 0
const type = this.type as 'server' | 'player' | 'weapon' | 'map' | 'gamemode'
if (this.type !== undefined) delete filter[type]
const data = this.store.getList(`${type}s`, filter)?.value
if (!data) return this.store.fetch(`${type}s`, filter).value.progress.value
return data.progress.value
}, },
colors () { colors () {
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
@@ -64,7 +75,7 @@ export default defineComponent({
} }
return colors return colors
}, },
chart () { chart (): ChartData<'doughnut', number[]> {
const colors = [ const colors = [
'cyan', 'cyan',
'green', 'green',
@@ -74,13 +85,12 @@ export default defineComponent({
'red', 'red',
'yellow' 'yellow'
] ]
const chartData = { const chartData: ChartData<'doughnut', number[]> = {
datasets: [ datasets: [
{ {
label: 'Weapons', label: 'Weapons',
labels: this.sortedWeaponList || ([] as string[]),
borderColor: () => this.colors.fg, borderColor: () => this.colors.fg,
backgroundColor: (context: any) => { backgroundColor: (context) => {
return this.colors[ return this.colors[
colors[(context.dataIndex * 3) % colors.length] colors[(context.dataIndex * 3) % colors.length]
] ]
@@ -89,41 +99,40 @@ export default defineComponent({
} }
] ]
} }
if (!this.sortedWeaponList) { if (!this.sortedList) {
return chartData return chartData
} }
chartData.datasets[0].data = this.sortedWeaponList.map((e) => { chartData.datasets[0].data = this.sortedList.map((e) => {
if (!this.weapons) { if (!this.values) {
return 0 return 0
} }
return this.weapons[e].value.kills return this.values[e].value.kills
}) })
return chartData return chartData
}, },
chartOptions () { chartOptions (): ChartOptions<'doughnut'> {
// eslint-disable-next-line no-unused-expressions // eslint-disable-next-line no-unused-expressions
this.$props.playerHighlighted this.$props.playerHighlighted
const options = { const options: ChartOptions<'doughnut'> = {
responsive: true, responsive: true,
maintainAspectRatio: true, maintainAspectRatio: true,
animation: { duration: 500 }, animation: { duration: 500 },
layout: { autoPadding: false }, layout: { autoPadding: false },
plugins: { plugins: {
datalabels: { datalabels: {
formatter: (value: any, context: any) => { formatter: (value, context) => {
const weaponId = this.sortedWeaponList[context.dataIndex] const weaponId = this.sortedList[context.dataIndex]
// if (!(weapons as {[key:string]:string})[weaponId]) console.log(weaponId) return this.locale[weaponId] || weaponId
return (weapons as {[key:string]:string})[weaponId] || weaponId
}, },
backgroundColor: (context: any) => { backgroundColor: (context) => {
return context.dataset.backgroundColor(context) return context.dataset.backgroundColor(context, options)
}, },
borderColor: this.colors.fg, borderColor: this.colors.fg,
borderRadius: 25, borderRadius: 25,
borderWidth: 2, borderWidth: 2,
color: this.colors.bg, color: this.colors.bg,
display: (context: any) => { display: (context) => {
return context.dataIndex === context.dataset.labels.length - 1 return context.dataIndex === context.dataset.data.length - 1
? true ? true
: 'auto' : 'auto'
}, },
@@ -133,12 +142,12 @@ export default defineComponent({
callbacks: { callbacks: {
label: (ctx: any) => { label: (ctx: any) => {
let label: string let label: string
if (!this.sortedWeaponList) { if (!this.sortedList) {
label = ctx.dataset.labels[ctx.dataIndex] label = ctx.dataset.labels[ctx.dataIndex]
} else { } else {
label = this.sortedWeaponList[ctx.dataIndex] label = this.sortedList[ctx.dataIndex]
} }
label += ' (' + this.weapons[label].value.kills + ' kills)' label += ' (' + this.values[label].value.kills + ' kills)'
return label return label
} }
} }
@@ -147,20 +156,23 @@ export default defineComponent({
} }
return options return options
}, },
weapons (): { [key: string]: Ref<Weapon> } { values (): { [key: string]: Ref<Weapon> } {
const { weapon: _, ...withoutWeapon } = this.filters || {} const filter = new Filter({ ...this.filters, player: this.playerHighlighted })
const data = this.store.getWeaponList(withoutWeapon)?.value.data if (this.type === undefined) return {}
if (!data) return this.store.fetchWeapons(withoutWeapon).value.data if (!(['server', 'player', 'weapon', 'map', 'gamemode'].includes(this.type))) return {}
const type = this.type as 'server' | 'player' | 'weapon' | 'map' | 'gamemode'
const data = this.store.getList(`${type}s`, filter)?.value.data
if (!data) return this.store.fetch(`${type}s`, filter).value.data
return data return data
}, },
sortedWeaponList (): string[] { sortedList (): string[] {
if (!this.weapons) return [] if (!this.values) return []
const weapons = Object.keys(this.weapons).filter(e => this.weapons[e].value.kills > 0) const weapons = Object.keys(this.values).filter(e => unref(this.values[e]).kills > 0)
weapons.sort((a, b) => { weapons.sort((a, b) => {
if (Number(this.weapons[a].value.kills) < Number(this.weapons[b].value.kills)) { if (Number(this.values[a].value.kills) < Number(unref(this.values[b]).kills)) {
return -1 return -1
} }
if (Number(this.weapons[a].value.kills) > Number(this.weapons[b].value.kills)) { if (Number(this.values[a].value.kills) > Number(unref(this.values[b]).kills)) {
return 1 return 1
} }
return 0 return 0
@@ -172,9 +184,8 @@ export default defineComponent({
</script> </script>
<style scoped> <style scoped>
.weaponChart { .chart {
width: calc(min(50vw, 50vh) - 3em); width: 100%;
height: calc(min(50vw, 50vh) - 3em);
} }
@media only screen and (max-width: 922px) { @media only screen and (max-width: 922px) {
+10 -8
View File
@@ -19,10 +19,10 @@ import {
} from 'chart.js' } from 'chart.js'
import annotationPlugin, { AnnotationPluginOptions } from 'chartjs-plugin-annotation' import annotationPlugin, { AnnotationPluginOptions } from 'chartjs-plugin-annotation'
import dataLabel from 'chartjs-plugin-datalabels' import dataLabel from 'chartjs-plugin-datalabels'
import { useKillStore, Player, Filters } from '@/stores/kill' import { useKillStore, Player, Filter } from '@/stores/kill'
import { Scatter } from 'vue-chartjs' import { Scatter } from 'vue-chartjs'
import { defineComponent, PropType, Ref, toRaw, triggerRef, unref } from 'vue' import { defineComponent, PropType, toRaw, unref } from 'vue'
import { ChartEvent } from 'chart.js/dist/core/core.plugins' import { ChartEvent } from 'chart.js/dist/core/core.plugins'
import { _DeepPartialObject } from 'chart.js/dist/types/utils' import { _DeepPartialObject } from 'chart.js/dist/types/utils'
@@ -33,7 +33,7 @@ ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend, annota
export default defineComponent({ export default defineComponent({
name: 'PlayerChart', name: 'PlayerChart',
props: { props: {
filters: Object as PropType<Filters>, filters: Object as PropType<Filter>,
playerHighlighted: String playerHighlighted: String
}, },
data: () => { data: () => {
@@ -53,14 +53,17 @@ export default defineComponent({
computed: { computed: {
progress () { progress () {
if (this.filters) { if (this.filters) {
const { player: _, ...withoutPlayer } = this.filters const filter = new Filter(this.filters)
return this.store.getPlayerList(withoutPlayer)?.value.progress delete filter.player
return this.store.getList('players', filter)?.value.progress.value
} }
return 0 return 0
}, },
playerList (): (Player & {id:string})[] { playerList (): (Player & {id:string})[] {
const data = this.store.getPlayerList(this.filters || {})?.value.data const filter = new Filter(this.filters)
if (!data) return Object.entries(this.store.fetchPlayers(this.filters || {}).value.data).map(e => ({ id: e[0], ...toRaw(e[1].value) })) delete filter.player
const data = unref(this.store.getList('players', filter))?.data
if (!data) return Object.entries(this.store.fetch('players', filter).value.data).map(e => ({ id: e[0], ...toRaw(e[1].value) }))
const cut = Object.entries(data).map(e => ({ id: e[0], ...toRaw(e[1].value) })) const cut = Object.entries(data).map(e => ({ id: e[0], ...toRaw(e[1].value) }))
cut.sort((a, b) => { cut.sort((a, b) => {
return b.kills - a.kills return b.kills - a.kills
@@ -140,7 +143,6 @@ export default defineComponent({
}, },
onClick: (e: ChartEvent, element: any) => { onClick: (e: ChartEvent, element: any) => {
this.$emit('highlightPlayer', element.length > 0 ? this.playerList[element[0].index].id : undefined) 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) => { onHover: (e: any, element: any) => {
if (!e.native.target) return if (!e.native.target) return
+20 -33
View File
@@ -28,15 +28,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType, Ref } from 'vue' import { defineComponent, PropType, Ref, isRef } from 'vue'
import { useKillStore, Player, Filters } from '@/stores/kill' import { useKillStore, Player, Filter } from '@/stores/kill'
import VirtualList from './List/VirtualList.vue' import VirtualList from './List/VirtualList.vue'
import LoadingBar from './LoadingBar.vue' import LoadingBar from './LoadingBar.vue'
export default defineComponent({ export default defineComponent({
components: { VirtualList, LoadingBar }, components: { VirtualList, LoadingBar },
props: { props: {
filters: { type: Object as PropType<Filters>, default: () => ({}) }, filters: { type: Object as PropType<Filter>, default: () => ({}) },
playerHighlighted: String playerHighlighted: String
}, },
emits: ['highlightPlayer'], emits: ['highlightPlayer'],
@@ -48,17 +48,25 @@ export default defineComponent({
}, },
computed: { computed: {
progress () { progress () {
const { player: _, ...withoutPlayer } = this.filters const filter = new Filter(this.filters)
return this.store.getPlayerList(withoutPlayer)?.value.progress delete filter.player
return this.store.getList('players', filter)?.value.progress.value
}, },
players (): { [key: string]: Ref<Player> } { players (): { [key: string]: Ref<Player> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars const filter = new Filter(this.filters)
const { player: _, ...withoutPlayer } = this.filters delete filter.player
const data = this.store.getPlayerList(withoutPlayer)?.value.data const data = this.store.getList('players', filter)?.value.data
if (!data) return this.store.fetchPlayers(withoutPlayer).value.data if (!data) return this.store.fetch('players', filter).value.data
return data return data
}, },
playerList (): (Ref<Player> & { id: string })[] { playerList (): (Ref<Player> & { id: string })[] {
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' })
const sortingFunctions = {
'k/d': ({ _rawValue: aVal }, { _rawValue: bVal }) => aVal.kills / Math.max(1, aVal.deaths) - bVal.kills / Math.max(1, bVal.deaths),
avg_distance: ({ _rawValue: aVal }, { _rawValue: bVal }) => ((aVal.total_distance / aVal.kills) || 0) - ((bVal.total_distance / bVal.kills) || 0),
username: ({ _rawValue: aVal }, { _rawValue: bVal }) => collator.compare(aVal.username, bVal.username)
} as {[k: string]: (a:Ref<Player>, b:Ref<Player>)=>number}
if (!this.players) return [] if (!this.players) return []
const players = Object.entries(this.players).map(e => { const players = Object.entries(this.players).map(e => {
const target = {} as ProxyHandler<Ref<Player>> const target = {} as ProxyHandler<Ref<Player>>
@@ -67,30 +75,10 @@ export default defineComponent({
return player return player
}) as (Ref<Player> & { id: string })[] }) as (Ref<Player> & { id: string })[]
if (this.sortingData.argument === 'username') { if (this.sortingData.argument in sortingFunctions) {
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }) players.sort(sortingFunctions[this.sortingData.argument])
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, 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, 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 { } else {
const argument = this.sortingData.argument const argument = this.sortingData.argument as Exclude<keyof Player, 'username' | 'avg_distance'>
players.sort((a, b) => { players.sort((a, b) => {
// Hack because toRaw is slow as fuck // Hack because toRaw is slow as fuck
const aVal = a._rawValue const aVal = a._rawValue
@@ -159,7 +147,6 @@ export default defineComponent({
} }
.playerTable { .playerTable {
grid-area: list;
margin-right: 1rem; margin-right: 1rem;
height: 100%; height: 100%;
user-select: none; user-select: none;
+44 -97
View File
@@ -3,7 +3,7 @@ import { createApp, ref, unref } from 'vue'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { useKillStore } from './stores/kill' import { Filter, Kill, KillData, useKillStore } from './stores/kill'
const pinia = createPinia() const pinia = createPinia()
@@ -44,121 +44,68 @@ type websocketData = {
// dev stuff // dev stuff
const store = useKillStore() const store = useKillStore()
const socket = new WebSocket('wss://tone.sleepycat.date/v2/client/websocket') const socket = new WebSocket('wss://toneapi.ovh/v2/client/websocket')
socket.onmessage = function (e) { socket.onmessage = function (e) {
if (e.data === 'ping') return socket.send('pong') if (e.data === 'ping') return socket.send('pong')
const data = JSON.parse(e.data) const data = JSON.parse(e.data)
registerWebSocketKill(data) // registerWebSocketKill(data)
} }
function registerWebSocketKill (data : websocketData) { function registerWebSocketKill (data : websocketData) {
store.$state.players.filter((e) => { const filter = new Filter(store.$state.currentFilter)
return ( delete filter.player
(!e.value.filter.server || const list = unref(store.getList('players', filter))
e.value.filter.server === data.servername) && if (!list) return
(!e.value.filter.weapon || if (!(!filter.server || filter.server.includes(data.servername))) return
e.value.filter.weapon === data.cause_of_death)
// Handle other types of filters once those are implemented const WeaponFilter = (!filter.weapon || filter.weapon.includes(data.cause_of_death))
) const CurrentWeaponFilter = filter.weapon?.includes(data.attacker_current_weapon)
}).forEach(e => { if (!(WeaponFilter || CurrentWeaponFilter)) return
if (!e.value.data[data.attacker_id]) {
e.value.data[data.attacker_id] = ref({ if (!(!filter.gamemode || filter.gamemode.includes(data.game_mode))) return
deaths: 0,
kills: 0, if (!list.data[data.victim_id]) {
max_distance: 0, list.data[data.victim_id] = ref({
total_distance: 0,
username: data.attacker_name
})
}
if (!e.value.data[data.victim_id]) {
e.value.data[data.victim_id] = ref({
deaths: 0, deaths: 0,
deaths_while_equipped: 0,
kills: 0, kills: 0,
max_distance: 0, max_distance: 0,
total_distance: 0, total_distance: 0,
username: data.victim_name username: data.victim_name
}) })
} }
// handle deaths with equipped, eventually if (WeaponFilter) {
/* unref(list.data[data.victim_id]).deaths++
if(e.value.filter.weapon){ } else if (CurrentWeaponFilter) {
!e.value.data[data.victim_id] = e.value.data[data.victim_id] = { unref(list.data[data.victim_id]).deaths_while_equipped++
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.victim_name,
} }
} */
unref(e.value.data[data.victim_id]).deaths++ if (!list.data[data.attacker_id]) {
unref(e.value.data[data.victim_id]).username = data.victim_name list.data[data.attacker_id] = ref({
unref(e.value.data[data.attacker_id]).username = data.attacker_name
if (data.attacker_id !== data.victim_id) {
unref(e.value.data[data.attacker_id]).kills++
unref(e.value.data[data.attacker_id]).total_distance += data.distance
unref(e.value.data[data.attacker_id]).max_distance = Math.max(data.distance, unref(e.value.data[data.attacker_id]).max_distance)
}
})
store.$state.weapons.filter((e) => {
return (
(!e.value.filter.server ||
e.value.filter.server === data.servername) &&
(!e.value.filter.player || e.value.filter.player === data.attacker_id)
// Handle other types of filters once those are implemented
)
}).forEach(e => {
if (!e.value.data[data.cause_of_death]) {
e.value.data[data.cause_of_death] = ref({
deaths: 0, deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
deaths_while_equipped: 0
})
}
// handle deaths with equipped, eventually
/*
if(e.value.filter.weapon){
!e.value.data[data.victim_id] = e.value.data[data.victim_id] = {
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.victim_name,
}
} */
if (data.attacker_id !== data.victim_id) {
unref(e.value.data[data.cause_of_death]).kills++
unref(e.value.data[data.cause_of_death]).total_distance += data.distance
unref(e.value.data[data.cause_of_death]).max_distance = Math.max(data.distance, unref(e.value.data[data.cause_of_death]).max_distance)
}
unref(e.value.data[data.cause_of_death]).deaths++
})
store.$state.servers.filter((e) => {
return (
(!e.value.filter.server ||
e.value.filter.server === data.servername)
// Handle other types of filters once those are implemented
)
}).forEach((e) => {
if (!e.value.data[data.servername]) {
e.value.data[data.servername] = ref({
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
deaths_while_equipped: 0, deaths_while_equipped: 0,
host: data.host kills: 0,
max_distance: 0,
total_distance: 0,
username: data.attacker_name
}) })
} }
if (data.attacker_id !== data.victim_id) {
unref(list.data[data.attacker_id]).kills++
unref(list.data[data.attacker_id]).total_distance += data.distance
unref(list.data[data.attacker_id]).max_distance = Math.max(data.distance, unref(list.data[data.attacker_id]).max_distance)
}
if (data.attacker_id !== data.victim_id) { unref(list.data[data.victim_id]).username = data.victim_name
unref(e.value.data[data.servername]).kills++ unref(list.data[data.attacker_id]).username = data.attacker_name
unref(e.value.data[data.servername]).total_distance += data.distance const serverlist = store.getList('servers')
unref(e.value.data[data.servername]).max_distance = Math.max(data.distance, unref(e.value.data[data.servername]).max_distance) if (serverlist) {
const server = unref(unref(serverlist).data[data.servername])
if (server) {
server.kills++
server.deaths++
server.total_distance += data.distance
server.max_distance = Math.max(data.distance, server.max_distance)
}
} }
unref(e.value.data[data.servername]).deaths++
})
} }
+28
View File
@@ -0,0 +1,28 @@
{
"private_match": "Private Match",
"aitdm": "Attrition",
"at": "Bounty Hunt",
"coliseum": "Coliseum",
"cp": "Amped Hardpoint",
"ctf": "Capture the Flag",
"fw": "Frontier War",
"lts": "Last Titan Standing",
"mfd": "Marked For Death",
"ps": "Pilots vs. Pilots",
"solo": "Campaign",
"tdm": "Skirmish",
"ttdm": "Titan Brawl",
"sns": "Stick and Stones",
"gg": "GunGame",
"fd": "Frontier Defense",
"ffa": "Free For All",
"chamber": "One in the Chamber",
"fastball": "Fastball",
"ctf_comp": "Competitive CTF",
"hidden":"The Hidden",
"hs":"Hide and Seek",
"inf":"Infection",
"kr":"Amped Killrace",
"tffa":"Titan FFA",
"tt":"Titan Tag"
}
+113 -120
View File
@@ -1,34 +1,34 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import * as assert from 'assert' import { Ref, ref, shallowRef, triggerRef, unref } from 'vue'
import { Ref, reactive, ref, shallowRef, triggerRef, unref } from 'vue'
export interface Filters { export class Filter {
server?: string; constructor (filt?:{server?:string[], player?: string, weapon?: string[], host?: string[], map?: string[], gamemode?: string[]}) {
if (filt?.server) this.server = filt.server
if (filt?.player) this.player = filt.player
if (filt?.weapon) this.weapon = filt.weapon
if (filt?.host) this.host = filt.host
if (filt?.map) this.map = filt.map
if (filt?.gamemode) this.gamemode = filt.gamemode
}
server?: string[];
player?: string; player?: string;
weapon?: string; weapon?: string[];
host?: string; host?: string[];
map?: string; map?: string[];
gamemode?: string; gamemode?: string[];
} toURLSearchParams (): URLSearchParams {
return new URLSearchParams(
function removeNullEntries (a: Filters) { Object.entries(this).filter(e => e[1] !== undefined)
return Object.fromEntries( .map<string[][]>((e) => [e[1]].flat().map((v) => [e[0], v]))
Object.entries(a).filter((e) => e[1] !== undefined && e[1] !== null) .flat(1)
) )
} }
function objectEqual (a: Filters, b: Filters) {
try {
a = removeNullEntries(a)
b = removeNullEntries(b)
assert.deepStrictEqual(a, b)
return true
} catch {
return false
}
} }
function fetchWithLoading (url: string, progress: (percentage: number) => void) { function fetchWithLoading (url: string, progress: (percentage: number) => void) {
return fetch(url).then(response => { return fetch(url)
.then((response) => {
if (!response.ok) { if (!response.ok) {
throw Error(response.status + ' ' + response.statusText) throw Error(response.status + ' ' + response.statusText)
} }
@@ -40,7 +40,9 @@ function fetchWithLoading (url: string, progress: (percentage: number) => void)
// to access headers, server must send CORS header "Access-Control-Expose-Headers: content-encoding, content-length x-file-size" // to access headers, server must send CORS header "Access-Control-Expose-Headers: content-encoding, content-length x-file-size"
// server must send custom x-file-size header if gzip or other content-encoding is used // server must send custom x-file-size header if gzip or other content-encoding is used
const contentEncoding = response.headers.get('Content-Encoding') const contentEncoding = response.headers.get('Content-Encoding')
const contentLength = response.headers.get(contentEncoding ? 'X-File-Size' : 'content-length') const contentLength = response.headers.get(
contentEncoding ? 'X-File-Size' : 'content-length'
)
if (contentLength === null) { if (contentLength === null) {
throw Error('Response size header unavailable') throw Error('Response size header unavailable')
} }
@@ -51,11 +53,16 @@ function fetchWithLoading (url: string, progress: (percentage: number) => void)
return new Response( return new Response(
new ReadableStream({ new ReadableStream({
start (controller) { start (controller) {
const reader = response.body!.getReader() if (response.body === null) {
throw new Error('response.body is null')
}
const reader = response.body.getReader()
read() read()
function read () { function read () {
reader.read().then(({ done, value }) => { reader
.read()
.then(({ done, value }) => {
if (done) { if (done) {
controller.close() controller.close()
progress(1) progress(1)
@@ -67,7 +74,8 @@ function fetchWithLoading (url: string, progress: (percentage: number) => void)
controller.enqueue(value) controller.enqueue(value)
} }
read() read()
}).catch(error => { })
.catch((error) => {
console.error(error) console.error(error)
controller.error(error) controller.error(error)
}) })
@@ -76,33 +84,25 @@ function fetchWithLoading (url: string, progress: (percentage: number) => void)
}) })
) )
}) })
.catch(error => { .catch((error) => {
console.error(error) console.error(error)
}) })
} }
export interface Kill { export interface Kill {
deaths: number; deaths: number;
deaths_while_equipped: number;
kills: number; kills: number;
// eslint-disable-next-line camelcase
max_distance: number; max_distance: number;
// eslint-disable-next-line camelcase
total_distance: number; total_distance: number;
} }
export interface KillData<T extends Kill> {
filter: Filters;
data: { [key: string]: Ref<T> };
progress?: number;
}
export interface Player extends Kill { export interface Player extends Kill {
username: string; username: string;
// eslint-disable-next-line camelcase
// deaths_while_equipped?: number; // deaths_while_equipped?: number;
} }
export interface Weapon extends Kill { export interface Weapon extends Kill {
// eslint-disable-next-line camelcase
deaths_while_equipped: number; deaths_while_equipped: number;
} }
@@ -110,6 +110,11 @@ export interface Server extends Kill {
host: number; host: number;
} }
export interface KillData<T extends Kill> {
data: { [key: string]: Ref<T> };
progress: Ref<number>;
}
export interface NSServer { export interface NSServer {
name: string; name: string;
region: string; region: string;
@@ -119,122 +124,110 @@ export interface NSServer {
map: string; map: string;
playlist: string; playlist: string;
hasPassword: boolean; hasPassword: boolean;
modInfo: { Mods: [] } modInfo: { Mods: [] };
} }
// define your typings for the store state // define your typings for the store state
export interface State { export interface State {
servers: Ref<KillData<Server>>[]; killData:{
players: Ref<KillData<Player>>[]; servers: Ref<Map<string, Ref<KillData<Server>>>>;
weapons: Ref<KillData<Weapon>>[]; players: Ref<Map<string, Ref<KillData<Player>>>>;
maps: Ref<KillData<Kill>>[]; weapons: Ref<Map<string, Ref<KillData<Weapon>>>>;
gamemodes: Ref<KillData<Kill>>[]; maps: Ref<Map<string, Ref<KillData<Kill>>>>;
gamemodes: Ref<Map<string, Ref<KillData<Kill>>>>;
},
hosts: { [key: number]: string }; hosts: { [key: number]: string };
nsServers: NSServer[] | undefined nsServers: NSServer[] | undefined;
currentFilter: Filter;
} }
type StateProperty = keyof State['killData']
type StateType<T extends StateProperty> =
T extends 'servers' ? Server :
T extends 'players' ? Player :
T extends 'weapons' ? Weapon :
Kill
let serverInterval: number let serverInterval: number
export const useKillStore = defineStore('kill', { export const useKillStore = defineStore('kill', {
state: (): State => ({ state: (): State => ({
servers: [], killData: {
players: [], servers: shallowRef(new Map()),
weapons: [], players: shallowRef(new Map()),
maps: [], weapons: shallowRef(new Map()),
gamemodes: [], maps: shallowRef(new Map()),
gamemodes: shallowRef(new Map())
},
hosts: {}, hosts: {},
nsServers: undefined nsServers: undefined,
currentFilter: new Filter()
}), }),
getters: { getters: {
getPlayerList: (state) => (filters: Filters) => { getList:
return state.players.find((e) => objectEqual(unref(e).filter, filters)) (state) =>
<T extends StateProperty>(type: T, filters?: Filter): Ref<KillData<StateType<T>>> | undefined => {
return unref(state.killData[type as keyof typeof state.killData]).get(filters?.toURLSearchParams().toString() ?? '') as Ref<KillData<StateType<T>>>
}, },
getWeaponList: (state) => (filters: Filters) =>
state.weapons.find((e) => objectEqual(unref(e).filter, filters)),
getServerList: (state) => (filters: Filters) =>
state.servers.find((e) => objectEqual(unref(e).filter, filters)),
getNSServers: (state) => state.nsServers getNSServers: (state) => state.nsServers
}, },
actions: { actions: {
fetchPlayers (filter: Filters) { fetch <T extends StateProperty> (type:T, filter?:Filter): Ref<KillData<StateType<T>>> {
filter = removeNullEntries(filter) let entry = this.getList(type, filter)
let entry = this.players.find((e) => objectEqual(unref(e).filter, filter))
if (entry === undefined) { if (entry === undefined) {
entry = shallowRef({ filter, data: {} }) entry = shallowRef<KillData<StateType<T>>>({ data: {}, progress: ref(0) })
this.players.push(entry) unref(this.$state.killData[type as keyof typeof this.$state.killData] as Map<string, Ref<KillData<StateType<T>>>>).set(filter?.toURLSearchParams().toString() ?? '', entry)
// console.log(isRef(entry), this.$state.killData)
} }
fetchWithLoading( fetchWithLoading(
'https://tone.sleepycat.date/v2/client/players?' + `https://toneapi.ovh/v2/client/${type}?` +
new URLSearchParams(filter as Record<keyof Filters, string>), filter?.toURLSearchParams() ?? '',
(progress) => { (progress) => {
if (entry && progress !== 1 && unref(entry).progress.value !== 1) {
unref(entry).progress.value = progress
triggerRef(unref(entry).progress)
}
}
).then(async (response) => {
if (entry) { if (entry) {
unref(entry).progress = progress entry.value.data = Object.fromEntries(
if (progress !== 1) triggerRef(entry) Object.entries(await response?.json()).map<[string, Ref<StateType<T>>]>((e) => [
} e[0],
} ref(e[1]) as Ref<StateType<T>>
).then(async response => { ])
if (entry) { )
unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Player)])) unref(entry).progress.value = 1
triggerRef(entry)
}
})
return entry
},
fetchWeapons (filter: Filters) {
filter = removeNullEntries(filter)
let entry = this.weapons.find((e) => objectEqual(unref(e).filter, filter))
if (!entry) {
entry = shallowRef({ filter, data: {} })
this.weapons.push(entry)
}
fetchWithLoading(
'https://tone.sleepycat.date/v2/client/weapons?' +
new URLSearchParams(filter as Record<keyof Filters, string>),
(progress) => {
if (entry) {
unref(entry).progress = progress
if (progress !== 1) triggerRef(entry)
}
}
).then(async response => {
if (entry) {
unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Weapon)]))
triggerRef(entry)
}
})
return entry
},
fetchServers (filter: Filters) {
filter = removeNullEntries(filter)
let entry = this.servers.find((e) => objectEqual(unref(e).filter, filter))
if (!entry) {
entry = shallowRef({ filter, data: {} })
this.servers.push(entry)
}
fetchWithLoading(
'https://tone.sleepycat.date/v2/client/servers?' +
new URLSearchParams(filter as Record<keyof Filters, string>),
(progress) => {
if (entry) {
unref(entry).progress = progress
if (progress !== 1) triggerRef(entry)
}
}
).then(async response => {
if (entry) {
unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Server)]))
triggerRef(entry) triggerRef(entry)
} }
}) })
return entry return entry
}, },
fetchNSServers () { fetchNSServers () {
fetch( fetch('https://northstar.tf/client/servers').then(async (response) => {
'https://northstar.tf/client/servers').then(async response => {
this.nsServers = await response.json() this.nsServers = await response.json()
}) })
clearInterval(serverInterval) clearInterval(serverInterval)
serverInterval = setInterval(() => this.fetchNSServers(), 60000) serverInterval = setInterval(() => this.fetchNSServers(), 60000)
return this.nsServers || [] return this.nsServers || []
},
setFilter (filter: Filter) {
if (this.currentFilter.player === filter.player) {
const newfilter = new Filter(filter)
delete newfilter.player
this.fetch('players', newfilter)
}
if (this.currentFilter.weapon === filter.weapon) {
const newfilter = new Filter(filter)
delete newfilter.weapon
this.fetch('weapons', newfilter)
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// const { server: _server, ...withoutServer } = filter
// if (this.currentFilter.server === _server) { this.fetchServers(withoutServer) }
this.currentFilter = filter
} }
} }
}) })
-5
View File
@@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>
+128 -40
View File
@@ -1,6 +1,12 @@
<template> <template>
<div id="filters"> <div id="filters">
<span class="multiselect-wrapper"> <span class="multiselect-wrapper">
<!-- group-values="servers"
group-label="id"
label="name"
:group-select="true"
:options="groupedServers" -->
<VueMultiselect <VueMultiselect
selectLabel="" selectLabel=""
deselectLabel="remove" deselectLabel="remove"
@@ -8,11 +14,29 @@
v-model="model.server" v-model="model.server"
:options="sortedServerList" :options="sortedServerList"
:allow-empty="true" :allow-empty="true"
:close-on-select="false"
:multiple="true"
> >
</VueMultiselect> </VueMultiselect>
<button @click="model.server = undefined" :disabled="!model.server">X</button> <button @click="model.server = undefined" :disabled="!model.server">X</button>
</span> </span>
<span class="multiselect-wrapper">
<VueMultiselect
selectLabel=""
deselectLabel="remove"
placeholder="Select gamemode"
v-model="model.gamemode"
:custom-label="((e: any) => gamemodeLocale[e] || e)"
:options="sortedGamemodeList"
:allow-empty="true"
:close-on-select="false"
:multiple="true"
>
</VueMultiselect>
<button @click="model.gamemode = undefined" :disabled="!model.gamemode">X</button>
</span>
<span class="multiselect-wrapper"> <span class="multiselect-wrapper">
<VueMultiselect <VueMultiselect
selectLabel="" selectLabel=""
@@ -26,7 +50,7 @@
</VueMultiselect> </VueMultiselect>
<button @click="model.weapon = undefined" :disabled="!model.weapon">X</button> <button @click="model.weapon = undefined" :disabled="!model.weapon">X</button>
</span> </span>
<button @click="applyFilters" :disabled="filters.server == model.server && filters.gamemode == model.gamemode && filters.weapon == model.weapon">Apply Filters</button>
<span class="multiselect-wrapper"> <span class="multiselect-wrapper">
<VueMultiselect <VueMultiselect
:options-limit="20" :options-limit="20"
@@ -56,60 +80,73 @@
:playerHighlighted="playerHighlighted?.id" :playerHighlighted="playerHighlighted?.id"
> >
</PlayerChart> </PlayerChart>
<WeaponChart <PieChart
:filters="{ player: playerHighlighted?.id, ...filters }" :filters="{ player: playerHighlighted?.id, ...filters }"
:playerHighlighted="playerHighlighted?.id" :playerHighlighted="playerHighlighted?.id"
> type="weapon"
</WeaponChart> ></PieChart>
<PieChart
:filters="{ player: playerHighlighted?.id, ...filters }"
:playerHighlighted="playerHighlighted?.id"
type="gamemode"
></PieChart>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Player, Weapon, Server, useKillStore, Filters } from '@/stores/kill' import { Player, Weapon, Server, Kill, useKillStore, Filter } from '@/stores/kill'
import PlayerList from '@/components/PlayerList.vue' import PlayerList from '@/components/PlayerList.vue'
import PlayerChart from '@/components/PlayerChart.vue' import PlayerChart from '@/components/PlayerChart.vue'
import WeaponChart from '@/components/WeaponChart.vue' import PieChart from '@/components/PieChart.vue'
import { Ref, defineComponent, unref } from 'vue' import { Ref, defineComponent, unref } from 'vue'
import VueMultiselect from 'vue-multiselect' import VueMultiselect from 'vue-multiselect'
import 'vue-multiselect/dist/vue-multiselect.css' import 'vue-multiselect/dist/vue-multiselect.css'
import weapons from '../stores/weapons.json' import weapons from '../stores/weapons.json'
import gamemodes from '../stores/gamemodes.json'
import router from '@/router' import router from '@/router'
function filterOutNull<T> (value:T | null):value is T {
return value !== null
}
export default defineComponent({ export default defineComponent({
name: 'PlayerView', name: 'PlayerView',
components: { components: {
VueMultiselect, VueMultiselect,
PlayerList, PlayerList,
PlayerChart, PlayerChart,
WeaponChart PieChart
}, },
data () { data () {
return { return {
model: { server: undefined, weapon: undefined } as unknown as { model: { } as unknown as {
weapon: string | undefined; weapon: string[] | undefined;
server: string | undefined; server: string[] | undefined;
gamemode: string[] | undefined;
}, },
filters: {} as Filters, filters: new Filter(),
store: useKillStore(), store: useKillStore(),
playerHighlighted: undefined as (Player & { id: string }) | undefined, playerHighlighted: undefined as (Player & { id: string }) | undefined,
weaponLocale: weapons as { [key: string]: string } weaponLocale: weapons as { [key: string]: string },
gamemodeLocale: gamemodes as { [key: string]: string }
} }
}, },
beforeCreate () { beforeCreate () {
let server let server
if (this.$route.query.server) server = this.$route.query.server?.toString() if (this.$route.query.server) server = [this.$route.query.server?.toString()]
this.model = { this.model = {
server, server,
weapon: this.$route.query.weapon?.toString() // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
weapon: [this.$route.query.weapon].flat().filter(e => e ?? false).map(e => e!.toString()),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
gamemode: [this.$route.query.gamemode].flat().filter(e => e ?? false).map(e => e!.toString())
} }
}, },
computed: { computed: {
servers (): { [key: string]: Ref<Server> } { servers (): { [key: string]: Ref<Server> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars const data = this.store.getList('servers')?.value.data
const { server: _, player: _1, ...withoutServer } = this.filters if (!data) return unref(this.store.fetch('servers')).data
const data = this.store.getServerList(withoutServer)?.value.data
if (!data) return unref(this.store.fetchServers(withoutServer)).data
return data return data
}, },
groupedServers () { groupedServers () {
@@ -127,17 +164,26 @@ export default defineComponent({
return hosts return hosts
}, },
weapons (): { [key: string]: Ref<Weapon> } { weapons (): { [key: string]: Ref<Weapon> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // const filter = new Filter(this.filters)
const { weapon: _, player: _1, ...withoutWeapons } = this.filters // delete filter.weapon
const data = this.store.getWeaponList(withoutWeapons)?.value.data // delete filter.player
if (!data) return this.store.fetchWeapons(withoutWeapons).value.data const data = this.store.getList('weapons')?.value.data
if (!data) return this.store.fetch('weapons').value.data
return data return data
}, },
players (): { [key: string]: Ref<Player> } { players (): { [key: string]: Ref<Player> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars const filter = new Filter(this.filters)
const { player: _, ...withoutPlayers } = this.filters delete filter.player
const data = this.store.getPlayerList(withoutPlayers)?.value.data const data = this.store.getList('players', filter)?.value.data
if (!data) return this.store.fetchPlayers(withoutPlayers).value.data if (!data) return this.store.fetch('players', filter).value.data
return data
},
gamemodes (): { [key: string]: Ref<Kill> } {
// const filter = new Filter(this.filters)
// delete filter.gamemode
// delete filter.player
const data = this.store.getList('gamemodes')?.value.data
if (!data) return this.store.fetch('gamemodes').value.data
return data return data
}, },
sortedWeaponList (): string[] { sortedWeaponList (): string[] {
@@ -150,6 +196,11 @@ export default defineComponent({
const servers = Object.keys(this.servers) const servers = Object.keys(this.servers)
return servers.sort() return servers.sort()
}, },
sortedGamemodeList (): string[] {
if (!this.gamemodes) return []
const gamemodes = Object.keys(this.gamemodes)
return gamemodes.sort()
},
sortedPlayerList (): Player[] { sortedPlayerList (): Player[] {
if (!this.players) return [] if (!this.players) return []
const players = Object.entries(this.players).map((e) => ({ const players = Object.entries(this.players).map((e) => ({
@@ -168,24 +219,26 @@ export default defineComponent({
} }
}, },
watch: { watch: {
model: {
handler () {
if (this.model.gamemode?.length === 0) this.model.gamemode = undefined
if (this.model.server?.length === 0) this.model.server = undefined
if (this.model.weapon?.length === 0) this.model.weapon = undefined
},
deep: true
},
players () { players () {
const player = this.$route.query.player?.toString() const player = this.$route.query.player?.toString()
if (player && this.playerHighlighted?.id !== player) { if (player && this.playerHighlighted?.id !== player) {
this.highlight_player(player) this.highlight_player(player)
} }
}, },
model: {
handler (newModel) {
this.filters.server = newModel.server
this.filters.weapon = newModel.weapon
},
deep: true
},
filters: { filters: {
handler (newValue) { handler (newValue) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { server: _, weapon: _2, ...withoutFilters } = this.$route.query const { server: _, weapon: _2, gamemode: _3, ...withoutFilters } = this.$route.query
router.push({ query: { ...newValue, ...withoutFilters } })// .then(e => { console.log(e) }) router.push({ query: { ...newValue, ...withoutFilters } })// .then(e => { console.log(e) })
this.store.setFilter({ player: this.store.currentFilter.player, ...newValue })
}, },
deep: true deep: true
}, },
@@ -194,6 +247,7 @@ export default defineComponent({
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { player: _, ...withoutPlayer } = this.$route.query 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) })
this.store.setFilter({ ...this.store.currentFilter, player: newValue?.id })
}, },
$route: { $route: {
handler: function () { handler: function () {
@@ -203,6 +257,11 @@ export default defineComponent({
} }
}, },
methods: { methods: {
applyFilters () {
this.filters.server = this.model.server
this.filters.weapon = this.model.weapon
this.filters.gamemode = this.model.gamemode
},
highlight_player (playerid: string) { highlight_player (playerid: string) {
const player = this.players[playerid] const player = this.players[playerid]
if (player) { if (player) {
@@ -219,18 +278,24 @@ export default defineComponent({
} }
} }
if (this.filters.weapon !== this.$route.query.weapon) { if (this.filters.weapon !== this.$route.query.weapon) {
this.model.weapon = this.$route.query.weapon?.toString() if (this.$route.query.weapon) this.model.weapon = [this.$route.query.weapon].flat().filter(filterOutNull).map(e => e.toString())
else this.model.weapon = undefined
} }
if (this.filters.server !== this.$route.query.server) { if (this.filters.server !== this.$route.query.server) {
if (this.$route.query.server) this.model.server = this.$route.query.server?.toString() if (this.$route.query.server) this.model.server = [this.$route.query.server].flat().filter(filterOutNull).map(e => e.toString())
else this.model.server = undefined else this.model.server = undefined
} }
if (this.filters.gamemode !== this.$route.query.gamemode) {
if (this.$route.query.gamemode) this.model.gamemode = [this.$route.query.gamemode].flat().filter(filterOutNull).map(e => e.toString())
else this.model.gamemode = undefined
}
this.applyFilters()
} }
} }
}) })
</script> </script>
<style scoped> <style>
.multiselect { .multiselect {
margin: 0 0.5em 1em 0.5em; margin: 0 0.5em 1em 0.5em;
} }
@@ -250,9 +315,9 @@ export default defineComponent({
display: grid; display: grid;
overflow: auto; overflow: auto;
grid-template-areas: grid-template-areas:
"list chart" "list chart chart"
"list info"; "list info1 info2";
grid-template-columns: 50% 50%; grid-template-columns: 50% 25% 25%;
grid-template-rows: 50% 50%; grid-template-rows: 50% 50%;
} }
@@ -279,11 +344,34 @@ export default defineComponent({
.playerChart { .playerChart {
display: none; display: none;
grid-area: "chart";
} }
.weaponChart { .weaponChart {
display: none; display: none;
grid-area: 'info1';
} }
.gamemodeChart{
display:none;
grid-area: 'info2';
}
.playerTable{
grid-area: 'list';
}
}
.playerChart {
grid-area: chart;
}
.weaponChart {
grid-area: info1;
}
.gamemodeChart{
grid-area: info2;
}
.playerTable{
grid-area: list;
} }
#filters { #filters {
+7 -2
View File
@@ -52,8 +52,8 @@ export default defineComponent({
}, },
computed: { computed: {
servers (): { [key: string]: Ref<Server> } { servers (): { [key: string]: Ref<Server> } {
const data = this.store.getServerList({})?.value.data const data = this.store.getList('servers')?.value.data
if (!data) return unref(this.store.fetchServers({})).data if (!data) return unref(this.store.fetch('servers')).data
return data return data
}, },
serverList (): (Ref<Server> & { name: string })[] { serverList (): (Ref<Server> & { name: string })[] {
@@ -88,10 +88,12 @@ export default defineComponent({
.offline { .offline {
color: var(--comment); color: var(--comment);
} }
.serverView { .serverView {
margin: 0 1rem 1rem 1rem; margin: 0 1rem 1rem 1rem;
overflow: auto; overflow: auto;
} }
.serverList { .serverList {
overflow-y: auto; overflow-y: auto;
height: 100%; height: 100%;
@@ -115,6 +117,7 @@ export default defineComponent({
height: 3em; height: 3em;
background: var(--current-line); background: var(--current-line);
} }
.serverHeaders>span { .serverHeaders>span {
padding: .5em .8em 0 .25em; padding: .5em .8em 0 .25em;
} }
@@ -134,6 +137,7 @@ export default defineComponent({
.serverRow>div { .serverRow>div {
padding: .5em .8em 0 .25em; padding: .5em .8em 0 .25em;
} }
.serverRow>div:not(:last-child) { .serverRow>div:not(:last-child) {
border-right: solid var(--bg-color) 2px; border-right: solid var(--bg-color) 2px;
} }
@@ -141,4 +145,5 @@ export default defineComponent({
img { img {
object-fit: contain; object-fit: contain;
height: 100%; height: 100%;
width: 100%;
}</style> }</style>
+1
View File
@@ -5,6 +5,7 @@
"module": "esnext", "module": "esnext",
"strict": true, "strict": true,
"jsx": "preserve", "jsx": "preserve",
"noImplicitAny": false,
"moduleResolution": "node", "moduleResolution": "node",
"experimentalDecorators": true, "experimentalDecorators": true,
"skipLibCheck": true, "skipLibCheck": true,