Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fbe38115f | ||
|
|
b827957d8a | ||
|
|
5ca3c56564 | ||
|
|
9a5ba1178d | ||
|
|
50adf2d9f0 | ||
|
|
979a65f443 | ||
|
|
77116443ec | ||
|
|
8d1167fc64 | ||
|
|
89bf501a2f | ||
|
|
6f85e223d0 | ||
|
|
ac31b7fb7a | ||
|
|
99272676cb | ||
|
|
7d38564107 | ||
|
|
93063e125c | ||
|
|
00f6016311 | ||
|
|
a6e75006e6 | ||
|
|
8bdfc2586a | ||
|
|
b26a971de1 | ||
|
|
9b7bf03e1c | ||
|
|
2f272e48e6 | ||
|
|
6d1c0d0d22 | ||
|
|
f35196ab4a | ||
|
|
51fbc60036 | ||
|
|
76b56c62ce | ||
|
|
10cf90bcf3 | ||
|
|
45cf95aa22 | ||
|
|
c17d9c4a73 | ||
|
|
288920ded8 | ||
|
|
0dbcadf0cf | ||
|
|
1e28a11007 | ||
|
|
e1394ce66c | ||
|
|
e489783022 | ||
|
|
5d330970c1 | ||
|
|
097afd085f | ||
|
|
78e3c6864b | ||
|
|
46a94b7898 | ||
|
|
72105b3a4c | ||
|
|
9764f65cf6 | ||
|
|
c565d1593b | ||
|
|
9446d7ec15 | ||
|
|
fb6684820b | ||
|
|
41b6ea08f9 | ||
|
|
388d9877b0 | ||
|
|
727a1d8e11 | ||
|
|
8e8408b2f8 | ||
|
|
14110c89e4 | ||
|
|
76db4c262d | ||
|
|
6c7a502785 | ||
|
|
e48ae26c51 | ||
|
|
ffd0db46d9 | ||
|
|
55312a19ff | ||
|
|
a97e088a52 | ||
|
|
b4d9749025 | ||
|
|
71698cd31c | ||
|
|
54aaff2f74 | ||
|
|
0d6cb316a3 | ||
|
|
5d5a39ab2a | ||
|
|
a8aa64d18b | ||
|
|
deb9db2704 | ||
|
|
9bce40819d | ||
|
|
b1a43aa7d4 | ||
|
|
155b8a1946 | ||
|
|
ef7806da03 | ||
|
|
f3b6779641 | ||
|
|
1eef943a0f | ||
|
|
cf2dba3648 | ||
|
|
52c45e53b8 | ||
|
|
224880dfc0 | ||
|
|
bf37a31802 | ||
|
|
341f81a6a5 | ||
|
|
2f5bbca42c | ||
|
|
0cbd5a0ef2 | ||
|
|
fdcfff2554 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -21,3 +21,5 @@ pnpm-debug.log*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
dist
|
||||
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -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>
|
||||
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 1.9 MiB |
|
Before Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 47 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
After Width: | Height: | Size: 245 KiB |
|
After Width: | Height: | Size: 2.3 MiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 276 KiB After Width: | Height: | Size: 269 KiB |
|
After Width: | Height: | Size: 512 KiB |
@@ -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>
|
||||
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -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
|
||||
@@ -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 Server 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 (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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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))
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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++
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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> = [
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div class="about">
|
||||
<h1>This is an about page</h1>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"module": "esnext",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"noImplicitAny": false,
|
||||
"moduleResolution": "node",
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||