diff --git a/src/components/LoadingBar.vue b/src/components/LoadingBar.vue new file mode 100644 index 0000000..791f2e6 --- /dev/null +++ b/src/components/LoadingBar.vue @@ -0,0 +1,25 @@ + + + + + + diff --git a/src/components/PlayerChart.vue b/src/components/PlayerChart.vue index b3c5f56..1523ee3 100644 --- a/src/components/PlayerChart.vue +++ b/src/components/PlayerChart.vue @@ -1,7 +1,7 @@ @@ -26,6 +26,8 @@ import { defineComponent, PropType, Ref, toRaw, triggerRef, unref } from 'vue' import { ChartEvent } from 'chart.js/dist/core/core.plugins' import { _DeepPartialObject } from 'chart.js/dist/types/utils' +import LoadingBar from './LoadingBar.vue' + ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend, annotationPlugin, dataLabel) export default defineComponent({ @@ -43,12 +45,19 @@ export default defineComponent({ }, emits: ['highlightPlayer'], components: { - Scatter + Scatter, LoadingBar }, mounted () { this.refreshColors++ }, computed: { + progress () { + if (this.filters) { + const { player: _, ...withoutPlayer } = this.filters + return this.store.getPlayerList(withoutPlayer)?.value.progress + } + 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) })) diff --git a/src/components/PlayerList.vue b/src/components/PlayerList.vue index 6d08032..c7594dd 100644 --- a/src/components/PlayerList.vue +++ b/src/components/PlayerList.vue @@ -12,7 +12,8 @@ Average distance - + +
{{ slotProps.index+1 }}
{{ slotProps.data.value.username }}
@@ -30,9 +31,10 @@ import { defineComponent, PropType, Ref } from 'vue' import { useKillStore, Player, Filters } 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, default: () => ({}) }, playerHighlighted: String @@ -45,6 +47,10 @@ export default defineComponent({ } }, computed: { + progress () { + const { player: _, ...withoutPlayer } = this.filters + return this.store.getPlayerList(withoutPlayer)?.value.progress + }, players (): { [key: string]: Ref } { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { player: _, ...withoutPlayer } = this.filters @@ -61,7 +67,6 @@ export default defineComponent({ return player }) as (Ref & { 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) => { diff --git a/src/components/WeaponChart.vue b/src/components/WeaponChart.vue index 4a57e9c..c5fd856 100644 --- a/src/components/WeaponChart.vue +++ b/src/components/WeaponChart.vue @@ -1,6 +1,7 @@ @@ -12,6 +13,8 @@ import { Doughnut } from 'vue-chartjs' import { defineComponent, PropType, Ref } from 'vue' import weapons from '../stores/weapons.json' +import LoadingBar from './LoadingBar.vue' + ChartJS.register(ArcElement, Tooltip, Legend, dataLabel) export default defineComponent({ @@ -22,7 +25,7 @@ export default defineComponent({ }, emits: ['highlightPlayer'], components: { - Doughnut + Doughnut, LoadingBar }, mounted () { this.refreshColors++ @@ -34,6 +37,10 @@ export default defineComponent({ } }, computed: { + progress () { + const { weapon: _, ...withoutWeapon } = this.filters || {} + return this.store.getWeaponList(withoutWeapon)?.value.progress + }, colors () { // eslint-disable-next-line no-unused-expressions this.refreshColors diff --git a/src/stores/kill.ts b/src/stores/kill.ts index f993c3a..96b082e 100644 --- a/src/stores/kill.ts +++ b/src/stores/kill.ts @@ -27,6 +27,59 @@ function objectEqual (a: Filters, b: Filters) { } } +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) { + const reader = response.body!.getReader() + + read() + function read () { + reader.read().then(({ done, value }) => { + if (done) { + controller.close() + 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; kills: number; @@ -38,6 +91,7 @@ export interface Kill { export interface KillData { filter: Filters; data: { [key: string]: Ref }; + progress?: number; } export interface Player extends Kill { @@ -56,15 +110,15 @@ export interface Server extends Kill { } export interface NSServer { - name:string; - region:string; - description:string; - playerCount:number; - maxPlayers:number; - map:string; - playlist:string; - hasPassword:boolean; - modInfo:{Mods:[]} + 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 { @@ -77,7 +131,7 @@ export interface State { nsServers: NSServer[] | undefined } -let serverInterval:number +let serverInterval: number export const useKillStore = defineStore('kill', { state: (): State => ({ servers: [], @@ -106,14 +160,20 @@ export const useKillStore = defineStore('kill', { entry = shallowRef({ filter, data: {} }) this.players.push(entry) } - fetch( + fetchWithLoading( 'https://tone.sleepycat.date/v2/client/players?' + - new URLSearchParams(filter as Record) + new URLSearchParams(filter as Record), + (progress) => { + if (entry) { + unref(entry).progress = progress + if (progress !== 1) triggerRef(entry) + } + } ).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!) + if (entry) { + unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Player)])) + triggerRef(entry) + } }) return entry }, @@ -125,14 +185,20 @@ export const useKillStore = defineStore('kill', { this.weapons.push(entry) } - fetch( + fetchWithLoading( 'https://tone.sleepycat.date/v2/client/weapons?' + - new URLSearchParams(filter as Record) + new URLSearchParams(filter as Record), + (progress) => { + if (entry) { + unref(entry).progress = progress + if (progress !== 1) triggerRef(entry) + } + } ).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!) + if (entry) { + unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Weapon)])) + triggerRef(entry) + } }) return entry }, @@ -143,14 +209,20 @@ export const useKillStore = defineStore('kill', { entry = shallowRef({ filter, data: {} }) this.servers.push(entry) } - fetch( + fetchWithLoading( 'https://tone.sleepycat.date/v2/client/servers?' + - new URLSearchParams(filter as Record) + new URLSearchParams(filter as Record), + (progress) => { + if (entry) { + unref(entry).progress = progress + if (progress !== 1) triggerRef(entry) + } + } ).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!) + if (entry) { + unref(entry).data = Object.fromEntries(Object.entries(await response?.json()).map(e => [e[0], ref(e[1] as Server)])) + triggerRef(entry) + } }) return entry },