mirror of
https://github.com/edg-l/ddstats.git
synced 2024-10-29 12:18:25 +00:00
virtual list
This commit is contained in:
parent
deec91941a
commit
e41564778c
|
@ -1,6 +1,8 @@
|
|||
<script lang="ts">
|
||||
export let href: string | null = null;
|
||||
export let target_ext: boolean = false;
|
||||
let clazz: string = '';
|
||||
export { clazz as class };
|
||||
</script>
|
||||
|
||||
{#if href != null}
|
||||
|
@ -9,16 +11,16 @@
|
|||
target={`${target_ext ? '_blank' : ''} `}
|
||||
class="inline-block text-sm px-4 py-2 leading-none border rounded
|
||||
text-teal-400 bg-gray-700 border-teal-500
|
||||
hover:border-transparent hover:bg-teal-500 hover:text-black lg:mt-0 font-bold"
|
||||
hover:border-transparent hover:bg-teal-500 hover:text-black lg:mt-0 font-bold {clazz || ''}"
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
on:click
|
||||
class="inline-block text-sm px-4 py-2 leading-none border rounded
|
||||
class="inline-block text-sm px-4 py-3 leading-none border rounded
|
||||
text-teal-400 bg-gray-700 border-teal-500
|
||||
hover:border-transparent hover:bg-teal-500 hover:text-black lg:mt-0 font-bold"
|
||||
hover:border-transparent hover:bg-teal-500 hover:text-black lg:mt-0 font-bold {clazz || ''}"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<script lang="ts">
|
||||
export let total: number;
|
||||
export let page: number = 0;
|
||||
// show 3 before and 3 after current page
|
||||
export let margin = 3;
|
||||
// show 1 before and 1 after current page
|
||||
export let margin = 1;
|
||||
let clazz: string | null | undefined = undefined;
|
||||
export { clazz as class };
|
||||
|
||||
|
|
|
@ -54,8 +54,19 @@
|
|||
</CardHeader>
|
||||
<div class="px-4 py-3">
|
||||
{#if server.info.map !== undefined && server.info.map.name !== undefined}
|
||||
<p title={server.info.map.sha256 || 'unknown sha256'}>Map: {server.info.map.name}</p>
|
||||
<p>Map Size: {(server.info.map.size / 1024).toFixed(2)} kb</p>
|
||||
<p title={server.info.map.sha256 || 'unknown sha256'}>
|
||||
Map: {server.info.map.name}
|
||||
{#if server.info.game_type === 'DDraceNetwork'}
|
||||
(<a class="text-teal-400 font-bold" href="https://ddnet.org/maps/{server.info.map.name}/">DDNet</a>)
|
||||
{/if}
|
||||
</p>
|
||||
<p>Map Size: {(server.info.map.size / 1024).toFixed(2)} KiB</p>
|
||||
{/if}
|
||||
{#if server.location !== undefined}
|
||||
<p>Location: {server.location.toUpperCase()}</p>
|
||||
{/if}
|
||||
{#if server.info.version !== undefined}
|
||||
<p>Version: {server.info.version}</p>
|
||||
{/if}
|
||||
|
||||
<p>Players: {(server.info.clients && server.info.clients.length) || 0} / {server.info.max_clients}</p>
|
||||
|
|
177
src/lib/components/VirtualList.svelte
Normal file
177
src/lib/components/VirtualList.svelte
Normal file
|
@ -0,0 +1,177 @@
|
|||
<script lang="ts">
|
||||
/*
|
||||
https://github.com/sveltejs/svelte-virtual-list
|
||||
Copyright (c) 2018 Rich Harris
|
||||
|
||||
Permission is hereby granted by the authors of this software, to any person, to use the software for any purpose, free of charge, including the rights to run, read, copy, change, distribute and sell it, and including usage rights to any patents the authors may hold on it, subject to the following conditions:
|
||||
|
||||
This license, or a link to its text, must be included with all copies of the software and any derivative works.
|
||||
|
||||
Any modification to the software submitted to the authors may be incorporated into the software under the terms of this license.
|
||||
|
||||
The software is provided "as is", without warranty of any kind, including but not limited to the warranties of title, fitness, merchantability and non-infringement. The authors have no obligation to provide support or updates for the software, and may not be held liable for any damages, claims or other liability arising from its use
|
||||
*/
|
||||
import { onMount, tick } from 'svelte';
|
||||
|
||||
// props
|
||||
export let items: any;
|
||||
export let height = '100%';
|
||||
export let itemHeight: any = undefined;
|
||||
|
||||
// read-only, but visible to consumers via bind:start
|
||||
export let start = 0;
|
||||
export let end = 0;
|
||||
|
||||
// local state
|
||||
let height_map: any = [];
|
||||
let rows: any;
|
||||
let viewport: any;
|
||||
let contents: any;
|
||||
let viewport_height = 0;
|
||||
let visible: any;
|
||||
let mounted: any;
|
||||
|
||||
let top = 0;
|
||||
let bottom = 0;
|
||||
let average_height: any;
|
||||
|
||||
$: visible = items.slice(start, end).map((data: any, i: any) => {
|
||||
return { index: i + start, data };
|
||||
});
|
||||
|
||||
// whenever `items` changes, invalidate the current heightmap
|
||||
$: if (mounted) refresh(items, viewport_height, itemHeight);
|
||||
|
||||
async function refresh(items: any, viewport_height: any, itemHeight: any) {
|
||||
const { scrollTop } = viewport;
|
||||
|
||||
await tick(); // wait until the DOM is up to date
|
||||
|
||||
let content_height = top - scrollTop;
|
||||
let i = start;
|
||||
|
||||
while (content_height < viewport_height && i < items.length) {
|
||||
let row = rows[i - start];
|
||||
|
||||
if (!row) {
|
||||
end = i + 1;
|
||||
await tick(); // render the newly visible row
|
||||
row = rows[i - start];
|
||||
}
|
||||
|
||||
const row_height = (height_map[i] = itemHeight || row.offsetHeight);
|
||||
content_height += row_height;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
end = i;
|
||||
|
||||
const remaining = items.length - end;
|
||||
average_height = (top + content_height) / end;
|
||||
|
||||
bottom = remaining * average_height;
|
||||
height_map.length = items.length;
|
||||
}
|
||||
|
||||
async function handle_scroll() {
|
||||
const { scrollTop } = viewport;
|
||||
|
||||
const old_start = start;
|
||||
|
||||
for (let v = 0; v < rows.length; v += 1) {
|
||||
height_map[start + v] = itemHeight || rows[v].offsetHeight;
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
let y = 0;
|
||||
|
||||
while (i < items.length) {
|
||||
const row_height = height_map[i] || average_height;
|
||||
if (y + row_height > scrollTop) {
|
||||
start = i;
|
||||
top = y;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
y += row_height;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
while (i < items.length) {
|
||||
y += height_map[i] || average_height;
|
||||
i += 1;
|
||||
|
||||
if (y > scrollTop + viewport_height) break;
|
||||
}
|
||||
|
||||
end = i;
|
||||
|
||||
const remaining = items.length - end;
|
||||
average_height = y / end;
|
||||
|
||||
while (i < items.length) height_map[i++] = average_height;
|
||||
bottom = remaining * average_height;
|
||||
|
||||
// prevent jumping if we scrolled up into unknown territory
|
||||
if (start < old_start) {
|
||||
await tick();
|
||||
|
||||
let expected_height = 0;
|
||||
let actual_height = 0;
|
||||
|
||||
for (let i = start; i < old_start; i += 1) {
|
||||
if (rows[i - start]) {
|
||||
expected_height += height_map[i];
|
||||
actual_height += itemHeight || rows[i - start].offsetHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const d = actual_height - expected_height;
|
||||
viewport.scrollTo(0, scrollTop + d);
|
||||
}
|
||||
|
||||
// TODO if we overestimated the space these
|
||||
// rows would occupy we may need to add some
|
||||
// more. maybe we can just call handle_scroll again?
|
||||
}
|
||||
|
||||
// trigger initial refresh
|
||||
onMount(() => {
|
||||
rows = contents.getElementsByTagName('svelte-virtual-list-row');
|
||||
mounted = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte-virtual-list-viewport
|
||||
bind:this={viewport}
|
||||
bind:offsetHeight={viewport_height}
|
||||
on:scroll={handle_scroll}
|
||||
style="height: {height};"
|
||||
>
|
||||
<svelte-virtual-list-contents bind:this={contents} style="padding-top: {top}px; padding-bottom: {bottom}px;">
|
||||
{#each visible as row (row.index)}
|
||||
<svelte-virtual-list-row>
|
||||
<slot item={row.data}>Missing template</slot>
|
||||
</svelte-virtual-list-row>
|
||||
{/each}
|
||||
</svelte-virtual-list-contents>
|
||||
</svelte-virtual-list-viewport>
|
||||
|
||||
<style>
|
||||
svelte-virtual-list-viewport {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
display: block;
|
||||
}
|
||||
|
||||
svelte-virtual-list-contents,
|
||||
svelte-virtual-list-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
svelte-virtual-list-row {
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
import type { ServerEntry } from '$lib';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Card from '$lib/components/Card.svelte';
|
||||
import Container from '$lib/components/Container.svelte';
|
||||
import Paginate from '$lib/components/Paginate.svelte';
|
||||
import ServerCard from '$lib/components/ServerCard.svelte';
|
||||
import VirtualList from '$lib/components/VirtualList.svelte';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
@ -24,34 +24,33 @@
|
|||
}
|
||||
});
|
||||
|
||||
let page = 0;
|
||||
let perPage = 10;
|
||||
|
||||
let totalPlayers = 0;
|
||||
let currentServers: ServerEntry[] = [];
|
||||
|
||||
$: {
|
||||
totalPlayers = data.servers.servers
|
||||
.filter((x) => x.info.clients !== undefined)
|
||||
.map((x) => x.info.clients.length)
|
||||
.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
$: {
|
||||
currentServers = data.servers.servers.slice(page * perPage, page * perPage + perPage);
|
||||
}
|
||||
</script>
|
||||
|
||||
<Button
|
||||
class="fixed bottom-8 right-8"
|
||||
on:click={() =>
|
||||
window.scrollTo({
|
||||
top: 0
|
||||
})}>Go to top</Button
|
||||
>
|
||||
|
||||
<Container>
|
||||
<Card class="px-4 py-3">
|
||||
<Card class="px-4 py-3 mb-4">
|
||||
<p class="font-bold text-4xl mb-2">Server Browser</p>
|
||||
<p>Here you can search through the master server list.</p>
|
||||
<p>Total players: {totalPlayers}</p>
|
||||
</Card>
|
||||
|
||||
<Card class="px-4 py-3 my-2 flex justify-center">
|
||||
<Paginate total={Math.floor(data.servers.servers.length / perPage)} bind:page />
|
||||
</Card>
|
||||
|
||||
{#each currentServers as server, index (page * perPage + index)}
|
||||
<ServerCard {server} />
|
||||
{/each}
|
||||
<VirtualList height="200vh" items={data.servers.servers} let:item>
|
||||
<!-- this will be rendered for each currently visible item -->
|
||||
<ServerCard server={item} />
|
||||
</VirtualList>
|
||||
</Container>
|
||||
|
|
Loading…
Reference in a new issue