73 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
legonzaur 0dbcadf0cf Merge branch 'master' of https://github.com/ToneAPI/ToneAPI_webclient 2023-06-01 10:29:22 +02:00
legonzaur 1e28a11007 Add lazy loading 2023-06-01 10:28:04 +02:00
legonzaur e1394ce66c Remove dist 2023-06-01 10:27:44 +02:00
legonzaur e489783022 'publish' 2023-05-31 13:27:38 +02:00
legonzaur 5d330970c1 fix list id width 2023-05-31 13:26:58 +02:00
legonzaur 097afd085f 'publish' 2023-05-26 16:04:08 +02:00
legonzaur 78e3c6864b remove consle logs 2023-05-26 16:03:51 +02:00
legonzaur 46a94b7898 Fix infinite loading for weapon chart 2023-05-26 16:03:01 +02:00
legonzaur 72105b3a4c 'publish' 2023-05-26 15:42:12 +02:00
legonzaur 9764f65cf6 add progress bar 2023-05-26 15:41:49 +02:00
legonzaur c565d1593b fix negative index count if list is too small 2023-05-26 13:18:33 +02:00
legonzaur 9446d7ec15 'publish' 2023-05-16 11:59:13 +02:00
legonzaur fb6684820b rename alternator image 2023-05-16 11:58:47 +02:00
legonzaur 41b6ea08f9 'publish' 2023-05-15 15:57:27 +02:00
legonzaur 388d9877b0 add weapons 2023-05-15 15:57:10 +02:00
legonzaur 727a1d8e11 'publish' 2023-05-15 10:57:58 +02:00
legonzaur 8e8408b2f8 fix players having 500 kills out of nowhere 2023-05-15 10:57:36 +02:00
legonzaur 14110c89e4 'publish' 2023-05-06 13:13:04 +02:00
legonzaur 76db4c262d change order of elements in server list 2023-05-06 13:12:48 +02:00
legonzaur 6c7a502785 'publish' 2023-05-06 13:03:32 +02:00
legonzaur e48ae26c51 fix empty space being taken if no image 2023-05-06 13:03:17 +02:00
legonzaur ffd0db46d9 'publish' 2023-05-06 12:59:15 +02:00
legonzaur 55312a19ff add meadow image 2023-05-06 12:59:00 +02:00
legonzaur a97e088a52 'publish' 2023-05-06 12:46:22 +02:00
legonzaur b4d9749025 Add caps to headers 2023-05-06 12:45:04 +02:00
legonzaur 71698cd31c Layout fixes for server view 2023-05-06 12:44:59 +02:00
legonzaur 54aaff2f74 remove links 2023-05-06 12:44:43 +02:00
legonzaur 0d6cb316a3 update og image 2023-05-06 12:44:36 +02:00
legonzaur 5d5a39ab2a update og _image 2023-05-06 12:34:31 +02:00
legonzaur a8aa64d18b fix playerList seek 2023-05-06 12:12:03 +02:00
legonzaur deb9db2704 fix list color 2023-05-06 11:44:05 +02:00
legonzaur 9bce40819d work on server list 2023-05-06 11:35:55 +02:00
legonzaur b1a43aa7d4 Sort servers by activity 2023-05-06 09:05:29 +02:00
legonzaur 155b8a1946 add relic 2023-05-06 09:04:12 +02:00
legonzaur ef7806da03 Merge branch 'master' into server 2023-05-06 09:01:32 +02:00
legonzaur f3b6779641 change title for embed 2023-05-05 21:56:34 +02:00
legonzaur 1eef943a0f 'publish' 2023-05-05 21:51:02 +02:00
legonzaur cf2dba3648 'publish' 2023-05-05 21:42:10 +02:00
legonzaur 52c45e53b8 fix for infinite redirection 2023-05-05 21:41:46 +02:00
legonzaur 224880dfc0 'publish' 2023-05-05 21:39:25 +02:00
legonzaur bf37a31802 add server preview 2023-05-05 21:39:04 +02:00
legonzaur 341f81a6a5 'publish' 2023-05-05 20:47:59 +02:00
legonzaur 2f5bbca42c fix server filter not applying for weapon object in realtime 2023-05-05 20:47:43 +02:00
legonzaur 0cbd5a0ef2 'publish' 2023-05-05 20:45:47 +02:00
legonzaur fdcfff2554 fix image path 2023-05-05 20:45:29 +02:00
88 changed files with 2476 additions and 3400 deletions
+6
View File
@@ -3,3 +3,9 @@ indent_style = space
indent_size = 2
trim_trailing_whitespace = 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 = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'@vue/standard',
'@vue/typescript/recommended'
],
parserOptions: {
ecmaVersion: 2020
},
rules: {
'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
+2
View File
@@ -21,3 +21,5 @@ pnpm-debug.log*
*.njsproj
*.sln
*.sw?
dist
-1
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

-1
View File
@@ -1 +0,0 @@
<!doctype html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><meta content="Tone API" property="og:title"/><meta content="Northstar kill statistics" property="og:description"/><meta content="https://toneapi.github.io/ToneAPI_webclient/" property="og:url"/><meta content="https://toneapi.github.io/ToneAPI_webclient/og_image.png" property="og:image"/><meta name="twitter:card" content="summary_large_image"><link rel="icon" href="/ToneAPI_webclient/favicon.ico"><title>toneapi_webclient</title><script defer="defer" src="/ToneAPI_webclient/js/chunk-vendors.3d141dfa.js"></script><script defer="defer" src="/ToneAPI_webclient/js/app.04e88cb3.js"></script><link href="/ToneAPI_webclient/css/app.2d4ee00d.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but toneapi_webclient doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div></body></html>
-2
View File
File diff suppressed because one or more lines are too long
-1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

+1589 -2706
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -26,9 +26,9 @@
"@typescript-eslint/eslint-plugin": "^5.4.0",
"@typescript-eslint/parser": "^5.4.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-typescript": "~5.0.0",
"@vue/cli-plugin-typescript": "~5.0.8",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-service": "~5.0.0",
"@vue/eslint-config-standard": "^6.1.0",
@@ -40,4 +40,4 @@
"eslint-plugin-vue": "^8.0.3",
"typescript": "~4.5.5"
}
}
}
+3 -3
View File
@@ -4,11 +4,11 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta content="Tone API" property="og:title" />
<meta content="Northstar kill statistics" property="og:description" />
<meta content="Players : Tone API" property="og:title" />
<meta content="Northstar players kills statistics" property="og:description" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/" property="og:url" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/og_image.png" property="og:image" />
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

+22
View File
@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta content="Servers : Tone API" property="og:title" />
<meta content="Northstar servers kills statistics" property="og:description" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/servers" property="og:url" />
<meta content="https://toneapi.github.io/ToneAPI_webclient/server_og_image.png?" property="og:image" />
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+3 -1
View File
@@ -1,4 +1,6 @@
call npm run build
call git add dist -f
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
+7 -6
View File
@@ -3,14 +3,11 @@
<nav>
<!-- <div class="navbar-element">hamburger</div> -->
<div class="navbar-logo"><a href="https://github.com/Legonzaur/Tone_WebClient" target="_blank"><img src="/tone_icon.png"/></a></div>
<div class="navbar-element github-link"><a href="https://github.com/Legonzaur/Tone_WebClient" target="_blank">Github</a></div>
<div class="navbar-element github-link"><a
href="https://github.com/Legonzaur/ToneAPI_servermod" target="_blank">Northstar&nbsp;Server&nbsp;Mod</a></div>
<div class="navbar-logo"><a href="https://github.com/ToneAPI" target="_blank"><img :src="`${publicPath}tone_icon.png`"/></a></div>
<div class="spacer"></div>
<!-- <router-link to="/" class="navbar-element">Home</router-link>
<router-link to="/about" class="navbar-element">About</router-link> -->
<router-link to="/servers" class="navbar-element">Servers</router-link>
<router-link to="/servers" class="navbar-element">Servers&nbsp;(Beta)</router-link>
<router-link to="/" class="navbar-element">Players</router-link>
</nav>
</header>
@@ -22,7 +19,11 @@
import { defineComponent } from 'vue'
export default defineComponent({
data () {
return {
publicPath: process.env.BASE_URL
}
}
})
</script>
-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>
+21 -8
View File
@@ -1,7 +1,9 @@
<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;`">
<slot v-for="(data, index) in visibleList" :data="data" :index="index"></slot>
<div ref="vscroll" class="vscroll">
<div :style="`padding: ${(interval[0]*$props.rowHeight!)}px 0px ${Math.max(0,(((list.length)*$props.rowHeight!)-(interval[1])*$props.rowHeight!))}px;`">
<slot v-for="(data, index) in visibleList" :data="data" :index="index+interval[0]"></slot>
</div>
</div>
</div>
</template>
@@ -25,14 +27,19 @@ export default defineComponent({
mounted () {
if (this.$props.list) {
this.vlistHeight = (this.$refs.vlist as HTMLElement)?.getBoundingClientRect().height || 0
this.toLoad = this.visibleCount * 3
if (this.$props.highlighted) {
setTimeout(() => (this.$refs.vlist as HTMLElement)?.scrollBy({ top: ((this.list.findIndex(e => e.id === this.$props.highlighted)) * this.rowHeight) - ((this.$refs.vlist as HTMLElement))?.scrollTop - (this.vlistHeight / 2 - this.$props.rowHeight), behavior: 'smooth' }), 1)
}
}
},
computed: {
interval () {
// console.log(this.list.length, this.vIndex + this.toLoad)
return [Math.max(Math.min(this.list.length - this.toLoad, this.vIndex), 0), Math.min(this.vIndex + this.toLoad, this.list.length)]
},
visibleList () {
return this.list.slice(Math.max(this.vIndex, 0), Math.min(this.vIndex + this.visibleCount * 3, this.list.length))
return this.list.slice(this.interval[0], this.interval[1])
},
visibleCount () {
return Math.ceil(this.vlistHeight / this.$props.rowHeight)
@@ -49,15 +56,21 @@ export default defineComponent({
methods: {
scroll (e:Event) {
const scrollTop = (e.target as HTMLElement)?.scrollTop
const scrollIndex = Math.round(scrollTop / this.$props.rowHeight)
const scrollIndex = Math.floor(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)
const mustScrollUp = scrollIndex < this.vIndex
const mustScrollDown = scrollIndex > this.vIndex + this.visibleCount
// console.log(this.vIndex + this.visibleCount, scrollIndex + this.visibleCount)
if (mustScrollUp) {
this.vIndex = Math.max(0, Math.floor((scrollIndex - this.visibleCount) / 4) * 4)
this.vIndex = Math.max(scrollIndex - this.visibleCount, 0)
} else if (mustScrollDown) {
this.vIndex = Math.max(0, Math.floor(scrollIndex / 4) * 4)
this.vIndex = Math.min(scrollIndex, this.list.length - this.toLoad)
}
// if (mustScrollDown || mustScrollUp) {
// const msg = (mustScrollDown ? 'down ' : '') + (mustScrollUp ? 'up ' : '')
// console.log(this.toLoad)
// console.log((this.list.length * this.$props.rowHeight) - (this.vIndex * this.$props.rowHeight) - (this.visibleCount * this.$props.rowHeight))
// }
}
}
+25
View File
@@ -0,0 +1,25 @@
<template>
<div class="loadingComponent">
<progress class="loadingBar" max="1" :value="value"></progress>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
value: Number
}
})
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
.loadingComponent {
display:flex;
align-items: center;
justify-content: center;
height: calc(100% - 3em);
}
</style>
+196
View File
@@ -0,0 +1,196 @@
<template>
<div class="chart" ref="container">
<LoadingBar v-if="progress !== 1" :value="progress"></LoadingBar>
<Doughnut :data="chart" :options="chartOptions" v-if="progress === 1" />
</div>
</template>
<script lang="ts">
import { Chart as ChartJS, ArcElement, Tooltip, Legend, ChartData, ChartOptions } from 'chart.js'
import dataLabel from 'chartjs-plugin-datalabels'
import { Weapon, Filter, useKillStore } from '@/stores/kill'
import { Doughnut } from 'vue-chartjs'
import { defineComponent, PropType, Ref, triggerRef, unref } from 'vue'
import LoadingBar from './LoadingBar.vue'
ChartJS.register(ArcElement, Tooltip, Legend, dataLabel)
export default defineComponent({
name: 'PieChart',
props: {
filters: Object as PropType<Filter>,
playerHighlighted: String,
type: Object as PropType<'server' | 'player' | 'weapon' | 'map' | 'gamemode'>
},
emits: ['highlightPlayer'],
components: {
Doughnut, LoadingBar
},
created () {
// eslint-disable-next-line no-extra-bind
import(`../stores/${this.type}s.json`).then((e => { this.locale = e }).bind(this))
},
mounted () {
this.refreshColors++
},
data: () => {
return {
store: useKillStore(),
refreshColors: 0,
locale: {} as {[key:string]:string}
}
},
computed: {
progress () {
const filter = new Filter(this.filters)
if (this.type === undefined) return 0
if (!(['server', 'player', 'weapon', 'map', 'gamemode'].includes(this.type))) return 0
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 () {
// eslint-disable-next-line no-unused-expressions
this.refreshColors
const colors = {} as { [key: string]: string }
if (this.$refs.container) {
const style = getComputedStyle(this.$refs.container as Element)
colors.fg = style.getPropertyValue('--foreground')
colors.bg = style.getPropertyValue('--bg-color')
colors.orange = style.getPropertyValue('--orange')
colors.cyan = style.getPropertyValue('--cyan')
colors.yellow = style.getPropertyValue('--yellow')
colors.green = style.getPropertyValue('--green')
colors.purple = style.getPropertyValue('--purple')
colors.red = style.getPropertyValue('--red')
colors.pink = style.getPropertyValue('--pink')
colors.currentLine = style.getPropertyValue('--current-line')
} else {
colors.fg = '#ffffff'
}
return colors
},
chart (): ChartData<'doughnut', number[]> {
const colors = [
'cyan',
'green',
'orange',
'pink',
'purple',
'red',
'yellow'
]
const chartData: ChartData<'doughnut', number[]> = {
datasets: [
{
label: 'Weapons',
borderColor: () => this.colors.fg,
backgroundColor: (context) => {
return this.colors[
colors[(context.dataIndex * 3) % colors.length]
]
},
data: [] as number[]
}
]
}
if (!this.sortedList) {
return chartData
}
chartData.datasets[0].data = this.sortedList.map((e) => {
if (!this.values) {
return 0
}
return this.values[e].value.kills
})
return chartData
},
chartOptions (): ChartOptions<'doughnut'> {
// eslint-disable-next-line no-unused-expressions
this.$props.playerHighlighted
const options: ChartOptions<'doughnut'> = {
responsive: true,
maintainAspectRatio: true,
animation: { duration: 500 },
layout: { autoPadding: false },
plugins: {
datalabels: {
formatter: (value, context) => {
const weaponId = this.sortedList[context.dataIndex]
return this.locale[weaponId] || weaponId
},
backgroundColor: (context) => {
return context.dataset.backgroundColor(context, options)
},
borderColor: this.colors.fg,
borderRadius: 25,
borderWidth: 2,
color: this.colors.bg,
display: (context) => {
return context.dataIndex === context.dataset.data.length - 1
? true
: 'auto'
},
padding: 6
},
tooltip: {
callbacks: {
label: (ctx: any) => {
let label: string
if (!this.sortedList) {
label = ctx.dataset.labels[ctx.dataIndex]
} else {
label = this.sortedList[ctx.dataIndex]
}
label += ' (' + this.values[label].value.kills + ' kills)'
return label
}
}
}
}
}
return options
},
values (): { [key: string]: Ref<Weapon> } {
const filter = new Filter({ ...this.filters, player: this.playerHighlighted })
if (this.type === undefined) return {}
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
},
sortedList (): string[] {
if (!this.values) return []
const weapons = Object.keys(this.values).filter(e => unref(this.values[e]).kills > 0)
weapons.sort((a, b) => {
if (Number(this.values[a].value.kills) < Number(unref(this.values[b]).kills)) {
return -1
}
if (Number(this.values[a].value.kills) > Number(unref(this.values[b]).kills)) {
return 1
}
return 0
})
return weapons
}
}
})
</script>
<style scoped>
.chart {
width: 100%;
}
@media only screen and (max-width: 922px) {
canvas {
display: none !important;
}
}
</style>
+20 -9
View File
@@ -1,7 +1,7 @@
<template>
<div class="playerChart" ref="container">
<!-- <Scatter :data="chart" :options="chartOptions" /> -->
<Scatter :data="chart" :options="chartOptions" />
<LoadingBar v-if="progress!==1" :value="progress"></LoadingBar>
<Scatter :data="chart" :options="chartOptions" v-if="progress===1" />
</div>
</template>
@@ -19,19 +19,21 @@ import {
} from 'chart.js'
import annotationPlugin, { AnnotationPluginOptions } from 'chartjs-plugin-annotation'
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 { 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 { _DeepPartialObject } from 'chart.js/dist/types/utils'
import LoadingBar from './LoadingBar.vue'
ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend, annotationPlugin, dataLabel)
export default defineComponent({
name: 'PlayerChart',
props: {
filters: Object as PropType<Filters>,
filters: Object as PropType<Filter>,
playerHighlighted: String
},
data: () => {
@@ -43,15 +45,25 @@ export default defineComponent({
},
emits: ['highlightPlayer'],
components: {
Scatter
Scatter, LoadingBar
},
mounted () {
this.refreshColors++
},
computed: {
progress () {
if (this.filters) {
const filter = new Filter(this.filters)
delete filter.player
return this.store.getList('players', filter)?.value.progress.value
}
return 0
},
playerList (): (Player & {id:string})[] {
const data = this.store.getPlayerList(this.filters || {})?.value.data
if (!data) return Object.entries(this.store.fetchPlayers(this.filters || {}).value.data).map(e => ({ id: e[0], ...toRaw(e[1].value) }))
const filter = new Filter(this.filters)
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) }))
cut.sort((a, b) => {
return b.kills - a.kills
@@ -131,7 +143,6 @@ 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
+33 -57
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
<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>
:class="sortingData.argument == 'avg_distance' ? 'selected' : ''">Average distance</span>
</div>
<VirtualList :list="playerList" :row-height="32" v-slot="slotProps">
<div :class="'playerRow ' + (slotProps.data.id === $props.playerHighlighted ? 'selected' : '')" v-on:click="$emit('highlightPlayer', slotProps.data.id)">
<LoadingBar v-if="progress!==1" :value="progress"></LoadingBar>
<VirtualList :list="playerList" :row-height="32" v-slot="slotProps" :highlighted="playerHighlighted" v-if="progress===1">
<div :class="'playerRow ' + (slotProps.data.id === playerHighlighted ? 'selected ' : '') + ((slotProps.index + 1)%2 ? 'odd ':'uneven ')" v-on:click="$emit('highlightPlayer', slotProps.data.id)">
<div><span>{{ slotProps.index+1 }}</span></div>
<div><span>{{ slotProps.data.value.username }}</span></div>
<div><span>{{ slotProps.data.value.kills }}</span></div>
@@ -23,29 +24,19 @@
<div><span>{{ Math.round(((slotProps.data.value.total_distance / slotProps.data.value.kills) || 0) * 100) / 100 }}</span></div>
</div>
</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>
<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>
</template>
<script lang="ts">
import { defineComponent, PropType, Ref } from 'vue'
import { useKillStore, Player, Filters } from '@/stores/kill'
import { defineComponent, PropType, Ref, isRef } from 'vue'
import { useKillStore, Player, Filter } from '@/stores/kill'
import VirtualList from './List/VirtualList.vue'
import LoadingBar from './LoadingBar.vue'
export default defineComponent({
components: { VirtualList },
components: { VirtualList, LoadingBar },
props: {
filters: { type: Object as PropType<Filters>, default: () => ({}) },
filters: { type: Object as PropType<Filter>, default: () => ({}) },
playerHighlighted: String
},
emits: ['highlightPlayer'],
@@ -56,14 +47,26 @@ export default defineComponent({
}
},
computed: {
progress () {
const filter = new Filter(this.filters)
delete filter.player
return this.store.getList('players', filter)?.value.progress.value
},
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
const filter = new Filter(this.filters)
delete filter.player
const data = this.store.getList('players', filter)?.value.data
if (!data) return this.store.fetch('players', filter).value.data
return data
},
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 []
const players = Object.entries(this.players).map(e => {
const target = {} as ProxyHandler<Ref<Player>>
@@ -72,31 +75,10 @@ export default defineComponent({
return player
}) 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, 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)
})
if (this.sortingData.argument in sortingFunctions) {
players.sort(sortingFunctions[this.sortingData.argument])
} else {
const argument = this.sortingData.argument
const argument = this.sortingData.argument as Exclude<keyof Player, 'username' | 'avg_distance'>
players.sort((a, b) => {
// Hack because toRaw is slow as fuck
const aVal = a._rawValue
@@ -118,11 +100,6 @@ export default defineComponent({
if (this.playerHighlighted) this.scrollToPlayer(this.playerHighlighted)
}
},
updated () {
/* const element = this.$refs['player:' + this.$props.playerHighlighted] as HTMLElement[]
if (!element || !element[0]) return
element[0].scrollIntoView({ behavior: 'smooth', block: 'center' }) */
},
methods: {
selectNextPlayer (e: KeyboardEvent) {
const values = {
@@ -170,7 +147,6 @@ export default defineComponent({
}
.playerTable {
grid-area: list;
margin-right: 1rem;
height: 100%;
user-select: none;
@@ -180,7 +156,7 @@ export default defineComponent({
.playerHeaders {
box-sizing: border-box;
display: grid;
grid-template-columns: 5ch 20ch 6ch 6ch 6ch 10ch 1fr;
grid-template-columns: 6ch 20ch 6ch 6ch 6ch 10ch 1fr;
background: var(--bg-color);
text-align: left;
cursor: pointer;
@@ -190,11 +166,11 @@ export default defineComponent({
height: 2em;
}
.playerRow:nth-child(2n+1) {
.playerRow.odd {
background: var(--accent);
}
.playerRow:nth-child(2n) {
.playerRow.uneven {
background: var(--bg-color);
}
@@ -248,7 +224,7 @@ export default defineComponent({
.playerRow,
.playerHeaders {
grid-template-columns: 5ch 20ch 6ch 6ch 1fr;
grid-template-columns: 6ch 20ch 6ch 6ch 1fr;
}
}
-176
View File
@@ -1,176 +0,0 @@
<template>
<div class="weaponChart" ref="container">
<Doughnut :data="chart" :options="chartOptions" />
</div>
</template>
<script lang="ts">
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 } from 'vue'
import weapons from '../stores/weapons.json'
ChartJS.register(ArcElement, Tooltip, Legend, dataLabel)
export default defineComponent({
name: 'WeaponChart',
props: {
filters: Object as PropType<Filters>,
playerHighlighted: String
},
emits: ['highlightPlayer'],
components: {
Doughnut
},
mounted () {
this.refreshColors++
},
data: () => {
return {
store: useKillStore(),
refreshColors: 0
}
},
computed: {
colors () {
// eslint-disable-next-line no-unused-expressions
this.refreshColors
const colors = {} as { [key: string]: string }
if (this.$refs.container) {
const style = getComputedStyle(this.$refs.container as Element)
colors.fg = style.getPropertyValue('--foreground')
colors.bg = style.getPropertyValue('--bg-color')
colors.orange = style.getPropertyValue('--orange')
colors.cyan = style.getPropertyValue('--cyan')
colors.yellow = style.getPropertyValue('--yellow')
colors.green = style.getPropertyValue('--green')
colors.purple = style.getPropertyValue('--purple')
colors.red = style.getPropertyValue('--red')
colors.pink = style.getPropertyValue('--pink')
colors.currentLine = style.getPropertyValue('--current-line')
} else {
colors.fg = '#ffffff'
}
return colors
},
chart () {
const colors = [
'cyan',
'green',
'orange',
'pink',
'purple',
'red',
'yellow'
]
const chartData = {
datasets: [
{
label: 'Weapons',
labels: this.sortedWeaponList || ([] as string[]),
borderColor: () => this.colors.fg,
backgroundColor: (context: any) => {
return this.colors[
colors[(context.dataIndex * 3) % colors.length]
]
},
data: [] as number[]
}
]
}
if (!this.sortedWeaponList) {
return chartData
}
chartData.datasets[0].data = this.sortedWeaponList.map((e) => {
if (!this.weapons) {
return 0
}
return this.weapons[e].value.kills
})
return chartData
},
chartOptions () {
// eslint-disable-next-line no-unused-expressions
this.$props.playerHighlighted
const options = {
responsive: true,
maintainAspectRatio: true,
animation: { duration: 500 },
layout: { autoPadding: false },
plugins: {
datalabels: {
formatter: (value: any, context: any) => {
const weaponId = this.sortedWeaponList[context.dataIndex]
// if (!(weapons as {[key:string]:string})[weaponId]) console.log(weaponId)
return (weapons as {[key:string]:string})[weaponId] || weaponId
},
backgroundColor: (context: any) => {
return context.dataset.backgroundColor(context)
},
borderColor: this.colors.fg,
borderRadius: 25,
borderWidth: 2,
color: this.colors.bg,
display: (context: any) => {
return context.dataIndex === context.dataset.labels.length - 1
? true
: 'auto'
},
padding: 6
},
tooltip: {
callbacks: {
label: (ctx: any) => {
let label: string
if (!this.sortedWeaponList) {
label = ctx.dataset.labels[ctx.dataIndex]
} else {
label = this.sortedWeaponList[ctx.dataIndex]
}
label += ' (' + this.weapons[label].value.kills + ' kills)'
return label
}
}
}
}
}
return options
},
weapons (): { [key: string]: Ref<Weapon> } {
const { weapon: _, ...withoutWeapon } = this.filters || {}
const data = this.store.getWeaponList(withoutWeapon)?.value.data
if (!data) return this.store.fetchWeapons(withoutWeapon).value.data
return data
},
sortedWeaponList (): string[] {
if (!this.weapons) return []
const weapons = Object.keys(this.weapons).filter(e => this.weapons[e].value.kills > 0)
weapons.sort((a, b) => {
if (Number(this.weapons[a].value.kills) < Number(this.weapons[b].value.kills)) {
return -1
}
if (Number(this.weapons[a].value.kills) > Number(this.weapons[b].value.kills)) {
return 1
}
return 0
})
return weapons
}
}
})
</script>
<style scoped>
.weaponChart {
width: calc(min(50vw, 50vh) - 3em);
height: calc(min(50vw, 50vh) - 3em);
}
@media only screen and (max-width: 922px) {
canvas {
display: none !important;
}
}
</style>
+55 -106
View File
@@ -3,7 +3,7 @@ import { createApp, ref, unref } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import { useKillStore } from './stores/kill'
import { Filter, Kill, KillData, useKillStore } from './stores/kill'
const pinia = createPinia()
@@ -44,119 +44,68 @@ type websocketData = {
// dev stuff
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) {
if (e.data === 'ping') return socket.send('pong')
const data = JSON.parse(e.data)
registerWebSocketKill(data)
// registerWebSocketKill(data)
}
function registerWebSocketKill (data : websocketData) {
store.$state.players.filter((e) => {
return (
(!e.value.filter.server ||
e.value.filter.server === data.servername) &&
(!e.value.filter.weapon ||
e.value.filter.weapon === data.cause_of_death)
// Handle other types of filters once those are implemented
)
}).forEach(e => {
if (!e.value.data[data.attacker_id]) {
e.value.data[data.attacker_id] = ref({
deaths: 0,
kills: 500,
max_distance: 0,
total_distance: 0,
username: data.attacker_name
})
}
if (!e.value.data[data.victim_id]) {
e.value.data[data.victim_id] = ref({
deaths: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.victim_name
})
}
// 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,
}
} */
const filter = new Filter(store.$state.currentFilter)
delete filter.player
const list = unref(store.getList('players', filter))
if (!list) return
if (!(!filter.server || filter.server.includes(data.servername))) return
unref(e.value.data[data.victim_id]).deaths++
unref(e.value.data[data.victim_id]).username = data.victim_name
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.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,
kills: 500,
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,
}
} */
const WeaponFilter = (!filter.weapon || filter.weapon.includes(data.cause_of_death))
const CurrentWeaponFilter = filter.weapon?.includes(data.attacker_current_weapon)
if (!(WeaponFilter || CurrentWeaponFilter)) return
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: 500,
max_distance: 0,
total_distance: 0,
deaths_while_equipped: 0,
host: data.host
})
}
if (!(!filter.gamemode || filter.gamemode.includes(data.game_mode))) return
if (data.attacker_id !== data.victim_id) {
unref(e.value.data[data.servername]).kills++
unref(e.value.data[data.servername]).total_distance += data.distance
unref(e.value.data[data.servername]).max_distance = Math.max(data.distance, unref(e.value.data[data.servername]).max_distance)
if (!list.data[data.victim_id]) {
list.data[data.victim_id] = ref({
deaths: 0,
deaths_while_equipped: 0,
kills: 0,
max_distance: 0,
total_distance: 0,
username: data.victim_name
})
}
if (WeaponFilter) {
unref(list.data[data.victim_id]).deaths++
} else if (CurrentWeaponFilter) {
unref(list.data[data.victim_id]).deaths_while_equipped++
}
if (!list.data[data.attacker_id]) {
list.data[data.attacker_id] = ref({
deaths: 0,
deaths_while_equipped: 0,
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)
}
unref(list.data[data.victim_id]).username = data.victim_name
unref(list.data[data.attacker_id]).username = data.attacker_name
const serverlist = store.getList('servers')
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++
})
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import ServerView from '../views/ServerView.vue'
import PlayerView from '../views/PlayerView.vue'
const ServerView = () => import('../views/ServerView.vue')
const PlayerView = () => import('../views/PlayerView.vue')
const routes: Array<RouteRecordRaw> = [
{
+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"
}
+183 -117
View File
@@ -1,53 +1,108 @@
import { defineStore } from 'pinia'
import * as assert from 'assert'
import { Ref, reactive, ref, shallowRef, triggerRef, unref } from 'vue'
import { Ref, ref, shallowRef, triggerRef, unref } from 'vue'
export interface Filters {
server?: string;
player?: string;
weapon?: string;
host?: string;
map?: string;
gamemode?: string;
}
function removeNullEntries (a: Filters) {
return Object.fromEntries(
Object.entries(a).filter((e) => e[1] !== undefined && e[1] !== null)
)
}
function objectEqual (a: Filters, b: Filters) {
try {
a = removeNullEntries(a)
b = removeNullEntries(b)
assert.deepStrictEqual(a, b)
return true
} catch {
return false
export class Filter {
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;
weapon?: string[];
host?: string[];
map?: string[];
gamemode?: string[];
toURLSearchParams (): URLSearchParams {
return new URLSearchParams(
Object.entries(this).filter(e => e[1] !== undefined)
.map<string[][]>((e) => [e[1]].flat().map((v) => [e[0], v]))
.flat(1)
)
}
}
function fetchWithLoading (url: string, progress: (percentage: number) => void) {
return fetch(url)
.then((response) => {
if (!response.ok) {
throw Error(response.status + ' ' + response.statusText)
}
if (!response.body) {
throw Error('ReadableStream not yet supported in this browser.')
}
// 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
const contentEncoding = response.headers.get('Content-Encoding')
const contentLength = response.headers.get(
contentEncoding ? 'X-File-Size' : 'content-length'
)
if (contentLength === null) {
throw Error('Response size header unavailable')
}
const total = parseInt(contentLength, 10)
let loaded = 0
return new Response(
new ReadableStream({
start (controller) {
if (response.body === null) {
throw new Error('response.body is null')
}
const reader = response.body.getReader()
read()
function read () {
reader
.read()
.then(({ done, value }) => {
if (done) {
controller.close()
progress(1)
return
}
if (value) {
loaded += value.byteLength
progress(loaded / total)
controller.enqueue(value)
}
read()
})
.catch((error) => {
console.error(error)
controller.error(error)
})
}
}
})
)
})
.catch((error) => {
console.error(error)
})
}
export interface Kill {
deaths: number;
deaths_while_equipped: number;
kills: number;
// eslint-disable-next-line camelcase
max_distance: number;
// eslint-disable-next-line camelcase
total_distance: number;
}
export interface KillData<T extends Kill> {
filter: Filters;
data: { [key: string]: Ref<T> };
}
export interface Player extends Kill {
username: string;
// eslint-disable-next-line camelcase
// deaths_while_equipped?: number;
}
export interface Weapon extends Kill {
// eslint-disable-next-line camelcase
deaths_while_equipped: number;
}
@@ -55,113 +110,124 @@ export interface Server extends Kill {
host: number;
}
export interface NSServer {
name:string;
region:string;
description:string;
playerCount:number;
maxPlayers:number;
map:string;
playlist:string;
hasPassword:boolean;
modInfo:{Mods:[]}
}
// define your typings for the store state
export interface State {
servers: Ref<KillData<Server>>[];
players: Ref<KillData<Player>>[];
weapons: Ref<KillData<Weapon>>[];
maps: Ref<KillData<Kill>>[];
gamemodes: Ref<KillData<Kill>>[];
hosts: { [key: number]: string };
nsServers: NSServer[] | undefined
export interface KillData<T extends Kill> {
data: { [key: string]: Ref<T> };
progress: Ref<number>;
}
let serverInterval:number
export interface NSServer {
name: string;
region: string;
description: string;
playerCount: number;
maxPlayers: number;
map: string;
playlist: string;
hasPassword: boolean;
modInfo: { Mods: [] };
}
// define your typings for the store state
export interface State {
killData:{
servers: Ref<Map<string, Ref<KillData<Server>>>>;
players: Ref<Map<string, Ref<KillData<Player>>>>;
weapons: Ref<Map<string, Ref<KillData<Weapon>>>>;
maps: Ref<Map<string, Ref<KillData<Kill>>>>;
gamemodes: Ref<Map<string, Ref<KillData<Kill>>>>;
},
hosts: { [key: number]: string };
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
export const useKillStore = defineStore('kill', {
state: (): State => ({
servers: [],
players: [],
weapons: [],
maps: [],
gamemodes: [],
killData: {
servers: shallowRef(new Map()),
players: shallowRef(new Map()),
weapons: shallowRef(new Map()),
maps: shallowRef(new Map()),
gamemodes: shallowRef(new Map())
},
hosts: {},
nsServers: undefined
nsServers: undefined,
currentFilter: new Filter()
}),
getters: {
getPlayerList: (state) => (filters: Filters) => {
return state.players.find((e) => objectEqual(unref(e).filter, filters))
},
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)),
getList:
(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>>>
},
getNSServers: (state) => state.nsServers
},
actions: {
fetchPlayers (filter: Filters) {
filter = removeNullEntries(filter)
let entry = this.players.find((e) => objectEqual(unref(e).filter, filter))
fetch <T extends StateProperty> (type:T, filter?:Filter): Ref<KillData<StateType<T>>> {
let entry = this.getList(type, filter)
if (entry === undefined) {
entry = shallowRef({ filter, data: {} })
this.players.push(entry)
}
fetch(
'https://tone.sleepycat.date/v2/client/players?' +
new URLSearchParams(filter as Record<keyof Filters, string>)
).then(async response => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
unref(entry)!.data = Object.fromEntries(Object.entries(await response.json()).map(e => [e[0], ref(e[1] as Player)]))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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)
entry = shallowRef<KillData<StateType<T>>>({ data: {}, progress: ref(0) })
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(
`https://toneapi.ovh/v2/client/${type}?` +
filter?.toURLSearchParams() ?? '',
(progress) => {
if (entry && progress !== 1 && unref(entry).progress.value !== 1) {
unref(entry).progress.value = progress
triggerRef(unref(entry).progress)
}
}
).then(async (response) => {
if (entry) {
entry.value.data = Object.fromEntries(
Object.entries(await response?.json()).map<[string, Ref<StateType<T>>]>((e) => [
e[0],
ref(e[1]) as Ref<StateType<T>>
])
)
unref(entry).progress.value = 1
fetch(
'https://tone.sleepycat.date/v2/client/weapons?' +
new URLSearchParams(filter as Record<keyof Filters, string>)
).then(async response => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
unref(entry)!.data = Object.fromEntries(Object.entries(await response.json()).map(e => [e[0], ref(e[1] as Weapon)]))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
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)
}
fetch(
'https://tone.sleepycat.date/v2/client/servers?' +
new URLSearchParams(filter as Record<keyof Filters, string>)
).then(async response => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
unref(entry)!.data = Object.fromEntries(Object.entries(await response.json()).map(e => [e[0], ref(e[1] as Server)]))
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
triggerRef(entry!)
triggerRef(entry)
}
})
return entry
},
fetchNSServers () {
fetch(
'https://northstar.tf/client/servers').then(async response => {
fetch('https://northstar.tf/client/servers').then(async (response) => {
this.nsServers = await response.json()
})
clearInterval(serverInterval)
serverInterval = setInterval(() => this.fetchNSServers(), 60000)
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>
+130 -43
View File
@@ -1,6 +1,12 @@
<template>
<div id="filters">
<span class="multiselect-wrapper">
<!-- group-values="servers"
group-label="id"
label="name"
:group-select="true"
:options="groupedServers" -->
<VueMultiselect
selectLabel=""
deselectLabel="remove"
@@ -8,11 +14,29 @@
v-model="model.server"
:options="sortedServerList"
:allow-empty="true"
:close-on-select="false"
:multiple="true"
>
</VueMultiselect>
<button @click="model.server = undefined" :disabled="!model.server">X</button>
</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">
<VueMultiselect
selectLabel=""
@@ -26,7 +50,7 @@
</VueMultiselect>
<button @click="model.weapon = undefined" :disabled="!model.weapon">X</button>
</span>
<button @click="applyFilters" :disabled="filters.server == model.server && filters.gamemode == model.gamemode && filters.weapon == model.weapon">Apply Filters</button>
<span class="multiselect-wrapper">
<VueMultiselect
:options-limit="20"
@@ -43,73 +67,86 @@
</div>
<div id="playerView">
<PlayerList
<PlayerList
:filters="filters"
v-on:highlightPlayer="highlight_player"
:playerHighlighted="playerHighlighted?.id"
>
</PlayerList>
<PlayerChart
<PlayerChart
:filters="filters"
v-on:highlightPlayer="highlight_player"
:playerHighlighted="playerHighlighted?.id"
>
</PlayerChart>
<WeaponChart
<PieChart
:filters="{ player: playerHighlighted?.id, ...filters }"
:playerHighlighted="playerHighlighted?.id"
>
</WeaponChart>
type="weapon"
></PieChart>
<PieChart
:filters="{ player: playerHighlighted?.id, ...filters }"
:playerHighlighted="playerHighlighted?.id"
type="gamemode"
></PieChart>
</div>
</template>
<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 PlayerChart from '@/components/PlayerChart.vue'
import WeaponChart from '@/components/WeaponChart.vue'
import PieChart from '@/components/PieChart.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'
import gamemodes from '../stores/gamemodes.json'
import router from '@/router'
function filterOutNull<T> (value:T | null):value is T {
return value !== null
}
export default defineComponent({
name: 'PlayerView',
components: {
VueMultiselect,
PlayerList,
PlayerChart,
WeaponChart
PieChart
},
data () {
return {
model: { server: undefined, weapon: undefined } as unknown as {
weapon: string | undefined;
server: string | undefined;
model: { } as unknown as {
weapon: string[] | undefined;
server: string[] | undefined;
gamemode: string[] | undefined;
},
filters: {} as Filters,
filters: new Filter(),
store: useKillStore(),
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 () {
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 = {
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: {
servers (): { [key: string]: Ref<Server> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { server: _, player: _1, ...withoutServer } = this.filters
const data = this.store.getServerList(withoutServer)?.value.data
if (!data) return unref(this.store.fetchServers(withoutServer)).data
const data = this.store.getList('servers')?.value.data
if (!data) return unref(this.store.fetch('servers')).data
return data
},
groupedServers () {
@@ -124,21 +161,29 @@ export default defineComponent({
.find((host) => host.id === server.host)
?.servers.push({ name: e[0], ...server })
})
console.log(hosts)
return hosts
},
weapons (): { [key: string]: Ref<Weapon> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { weapon: _, player: _1, ...withoutWeapons } = this.filters
const data = this.store.getWeaponList(withoutWeapons)?.value.data
if (!data) return this.store.fetchWeapons(withoutWeapons).value.data
// const filter = new Filter(this.filters)
// delete filter.weapon
// delete filter.player
const data = this.store.getList('weapons')?.value.data
if (!data) return this.store.fetch('weapons').value.data
return data
},
players (): { [key: string]: Ref<Player> } {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { player: _, ...withoutPlayers } = this.filters
const data = this.store.getPlayerList(withoutPlayers)?.value.data
if (!data) return this.store.fetchPlayers(withoutPlayers).value.data
const filter = new Filter(this.filters)
delete filter.player
const data = this.store.getList('players', filter)?.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
},
sortedWeaponList (): string[] {
@@ -151,6 +196,11 @@ export default defineComponent({
const servers = Object.keys(this.servers)
return servers.sort()
},
sortedGamemodeList (): string[] {
if (!this.gamemodes) return []
const gamemodes = Object.keys(this.gamemodes)
return gamemodes.sort()
},
sortedPlayerList (): Player[] {
if (!this.players) return []
const players = Object.entries(this.players).map((e) => ({
@@ -169,24 +219,26 @@ export default defineComponent({
}
},
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 () {
const player = this.$route.query.player?.toString()
if (player && this.playerHighlighted?.id !== player) {
this.highlight_player(player)
}
},
model: {
handler (newModel) {
this.filters.server = newModel.server
this.filters.weapon = newModel.weapon
},
deep: true
},
filters: {
handler (newValue) {
// 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) })
this.store.setFilter({ player: this.store.currentFilter.player, ...newValue })
},
deep: true
},
@@ -195,6 +247,7 @@ export default defineComponent({
// 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) })
this.store.setFilter({ ...this.store.currentFilter, player: newValue?.id })
},
$route: {
handler: function () {
@@ -204,6 +257,11 @@ export default defineComponent({
}
},
methods: {
applyFilters () {
this.filters.server = this.model.server
this.filters.weapon = this.model.weapon
this.filters.gamemode = this.model.gamemode
},
highlight_player (playerid: string) {
const player = this.players[playerid]
if (player) {
@@ -220,18 +278,24 @@ export default defineComponent({
}
}
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.$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
}
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>
<style scoped>
<style>
.multiselect {
margin: 0 0.5em 1em 0.5em;
}
@@ -251,9 +315,9 @@ export default defineComponent({
display: grid;
overflow: auto;
grid-template-areas:
"list chart"
"list info";
grid-template-columns: 50% 50%;
"list chart chart"
"list info1 info2";
grid-template-columns: 50% 25% 25%;
grid-template-rows: 50% 50%;
}
@@ -280,13 +344,36 @@ export default defineComponent({
.playerChart {
display: none;
grid-area: "chart";
}
.weaponChart {
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 {
grid-area: filters;
}
+85 -43
View File
@@ -1,28 +1,37 @@
<template>
<div id="filters">
<div id="filters">
</div>
<VirtualList :list="serverList" :row-height="100" v-slot="slotProps">
</div>
<div class="serverView">
<div class="serverList">
<div class="serverHeaders">
<span></span>
<span>Map</span>
<span>Server name</span>
<span>Players</span>
<span>Kills</span>
<span>Max distance</span>
<span>Average distance</span>
</div>
<VirtualList :list="serverList" :row-height="100" v-slot="slotProps">
<div
:class="'serverRow ' + (!nsServersByName[slotProps.data.name] ? 'offline ' : ' ') + (!nsServersByName[slotProps.data.name]?.playerCount ? 'inactive' : '')">
<div><span>{{ slotProps.index + 1 }}</span></div>
<div><span>{{ slotProps.data.name }}</span></div>
<div><span>{{ slotProps.data._value.kills }}</span></div>
<div><span>{{ slotProps.data._value.max_distance }}</span></div>
<div><span>{{ Math.round(((slotProps.data._value.total_distance / slotProps.data._value.kills) || 0) * 100) /
100 }}</span></div>
<img v-if="nsServersByName[slotProps.data.name]"
:src="`${publicPath}maps/${nsServersByName[slotProps.data.name].map}_lobby.png`" />
</div>
<!-- <div :class="(!nsServersByName[slotProps.data.servername] ? 'offline ' : ' ') + (!nsServersByName[slotProps.data.servername]?.playerCount ? 'inactive' : '')">
{{ slotProps.data.servername }} {{ slotProps.data.value.kills }} {{ nsServersByName[slotProps.data.servername] ? nsServersByName[slotProps.data.servername]?.playerCount +'/'+nsServersByName[slotProps.data.servername]?.maxPlayers : ''}}
<img v-if="nsServersByName[slotProps.data.servername]" :src="`${publicPath}maps/${nsServersByName[slotProps.data.servername].map}_lobby.png`"/>
</div> -->
</VirtualList>
<div>
:class="'serverRow ' + (!nsServersByName[slotProps.data.name] ? 'offline ' : '') + (!nsServersByName[slotProps.data.name]?.playerCount ? 'inactive ' : '') + ((slotProps.index + 1) % 2 ? 'odd ' : 'uneven ')">
<div><span>{{ slotProps.index + 1 }}</span></div>
<img v-if="nsServersByName[slotProps.data.name]"
:src="`${publicPath}maps/${nsServersByName[slotProps.data.name].map}_lobby.png`" />
<div v-if="!nsServersByName[slotProps.data.name]"></div>
<div><span>{{ slotProps.data.name + (!nsServersByName[slotProps.data.name] ? ' (offline)' : '') }}</span></div>
<div><span>{{ nsServersByName[slotProps.data.name] ? nsServersByName[slotProps.data.name]?.playerCount
+ '/' + nsServersByName[slotProps.data.name]?.maxPlayers : '' }}</span></div>
<div><span>{{ slotProps.data._value.deaths }}</span></div>
<div><span>{{ slotProps.data._value.max_distance }}</span></div>
<div><span>{{ Math.round(((slotProps.data._value.total_distance / slotProps.data._value.kills) || 0) * 100) /
100 }}</span></div>
</div>
</VirtualList>
</div>
</div>
</template>
<script lang="ts">
@@ -43,19 +52,24 @@ export default defineComponent({
},
computed: {
servers (): { [key: string]: Ref<Server> } {
const data = this.store.getServerList({})?.value.data
if (!data) return unref(this.store.fetchServers({})).data
const data = this.store.getList('servers')?.value.data
if (!data) return unref(this.store.fetch('servers')).data
return data
},
serverList (): (Ref<Server> & { name: string })[] {
return Object.entries(this.servers).map(e => ({ ...e[1], name: e[0] }))
const data = Object.entries(this.servers).map(e => ({ ...e[1], name: e[0] }))
data.sort((server1, server2) => {
const a = !this.nsServersByName[server1.name] ? -1 : this.nsServersByName[server1.name].playerCount ? 1 : 0
const b = !this.nsServersByName[server2.name] ? -1 : this.nsServersByName[server2.name].playerCount ? 1 : 0
return b - a
})
return data
},
nsServers (): NSServer[] {
const data = this.store.getNSServers
if (!data) {
return this.store.fetchNSServers()
}
console.log(data)
return data
},
nsServersByName (): { [key: string]: NSServer } {
@@ -68,40 +82,68 @@ export default defineComponent({
<style scoped>
.inactive {
color: var(--comment);
color: var(--comment);
}
.offline {
color: var(--current-line);
color: var(--comment);
}
.serverView {
margin: 0 1rem 1rem 1rem;
overflow: auto;
}
.serverList {
overflow-y: auto;
height: 100%;
}
.serverRow,
.serverHeaders {
box-sizing: border-box;
display: grid;
grid-template-columns: 5ch 20ch 6ch 6ch 6ch 10ch 1fr;
background: var(--bg-color);
text-align: left;
cursor: pointer;
margin: 0;
/* border: 2px solid transparent; */
transition: border .25s;
height: 100px;
box-sizing: border-box;
display: grid;
grid-template-columns: 4ch 178px 20ch 8ch 8ch 8ch 1fr;
background: var(--bg-color);
text-align: left;
cursor: pointer;
margin: 0;
/* border: 2px solid transparent; */
transition: border .25s;
height: 100px;
}
.serverRow:nth-child(2n+1) {
background: var(--accent);
.serverHeaders {
height: 3em;
background: var(--current-line);
}
.serverRow:nth-child(2n) {
background: var(--bg-color);
.serverHeaders>span {
padding: .5em .8em 0 .25em;
}
.serverRow.odd {
background: var(--accent);
}
.serverRow.uneven {
background: var(--bg-color);
}
.serverRow:hover {
background: var(--current-line);
background: var(--current-line);
}
.serverRow>div {
padding: .5em .8em 0 .25em;
}
.serverRow>div:not(:last-child) {
border-right: solid var(--bg-color) 2px;
}
img {
object-fit: contain;
height:100%
object-fit: contain;
height: 100%;
width: 100%;
}</style>
+1
View File
@@ -5,6 +5,7 @@
"module": "esnext",
"strict": true,
"jsx": "preserve",
"noImplicitAny": false,
"moduleResolution": "node",
"experimentalDecorators": true,
"skipLibCheck": true,
+16 -1
View File
@@ -1,5 +1,20 @@
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
productionSourceMap: process.env.NODE_ENV !== 'production',
transpileDependencies: true,
publicPath: process.env.NODE_ENV === 'production' ? '/ToneAPI_webclient/' : '/'
publicPath: process.env.NODE_ENV === 'production' ? '/ToneAPI_webclient/' : '/',
pages: {
index: {
entry: 'src/main.ts',
template: 'public/index.html',
filename: 'index.html',
title: 'Players : Tone API'
},
server: {
entry: 'src/main.ts',
template: 'public/servers.html',
filename: 'servers.html',
title: 'Servers : Tone API'
}
}
})