virtual list

This commit is contained in:
Edgar 2023-07-22 11:06:55 +02:00
parent deec91941a
commit e41564778c
5 changed files with 213 additions and 24 deletions

View file

@ -1,6 +1,8 @@
<script lang="ts"> <script lang="ts">
export let href: string | null = null; export let href: string | null = null;
export let target_ext: boolean = false; export let target_ext: boolean = false;
let clazz: string = '';
export { clazz as class };
</script> </script>
{#if href != null} {#if href != null}
@ -9,16 +11,16 @@
target={`${target_ext ? '_blank' : ''} `} target={`${target_ext ? '_blank' : ''} `}
class="inline-block text-sm px-4 py-2 leading-none border rounded class="inline-block text-sm px-4 py-2 leading-none border rounded
text-teal-400 bg-gray-700 border-teal-500 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 /> <slot />
</a> </a>
{:else} {:else}
<button <button
on:click 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 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 /> <slot />
</button> </button>

View file

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
export let total: number; export let total: number;
export let page: number = 0; export let page: number = 0;
// show 3 before and 3 after current page // show 1 before and 1 after current page
export let margin = 3; export let margin = 1;
let clazz: string | null | undefined = undefined; let clazz: string | null | undefined = undefined;
export { clazz as class }; export { clazz as class };

View file

@ -54,8 +54,19 @@
</CardHeader> </CardHeader>
<div class="px-4 py-3"> <div class="px-4 py-3">
{#if server.info.map !== undefined && server.info.map.name !== undefined} {#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 title={server.info.map.sha256 || 'unknown sha256'}>
<p>Map Size: {(server.info.map.size / 1024).toFixed(2)} kb</p> 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} {/if}
<p>Players: {(server.info.clients && server.info.clients.length) || 0} / {server.info.max_clients}</p> <p>Players: {(server.info.clients && server.info.clients.length) || 0} / {server.info.max_clients}</p>

View 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>

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import type { ServerEntry } from '$lib'; import Button from '$lib/components/Button.svelte';
import Card from '$lib/components/Card.svelte'; import Card from '$lib/components/Card.svelte';
import Container from '$lib/components/Container.svelte'; import Container from '$lib/components/Container.svelte';
import Paginate from '$lib/components/Paginate.svelte';
import ServerCard from '$lib/components/ServerCard.svelte'; import ServerCard from '$lib/components/ServerCard.svelte';
import VirtualList from '$lib/components/VirtualList.svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
@ -24,34 +24,33 @@
} }
}); });
let page = 0;
let perPage = 10;
let totalPlayers = 0; let totalPlayers = 0;
let currentServers: ServerEntry[] = [];
$: { $: {
totalPlayers = data.servers.servers totalPlayers = data.servers.servers
.filter((x) => x.info.clients !== undefined) .filter((x) => x.info.clients !== undefined)
.map((x) => x.info.clients.length) .map((x) => x.info.clients.length)
.reduce((a, b) => a + b, 0); .reduce((a, b) => a + b, 0);
} }
$: {
currentServers = data.servers.servers.slice(page * perPage, page * perPage + perPage);
}
</script> </script>
<Button
class="fixed bottom-8 right-8"
on:click={() =>
window.scrollTo({
top: 0
})}>Go to top</Button
>
<Container> <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 class="font-bold text-4xl mb-2">Server Browser</p>
<p>Here you can search through the master server list.</p> <p>Here you can search through the master server list.</p>
<p>Total players: {totalPlayers}</p> <p>Total players: {totalPlayers}</p>
</Card> </Card>
<Card class="px-4 py-3 my-2 flex justify-center"> <VirtualList height="200vh" items={data.servers.servers} let:item>
<Paginate total={Math.floor(data.servers.servers.length / perPage)} bind:page /> <!-- this will be rendered for each currently visible item -->
</Card> <ServerCard server={item} />
</VirtualList>
{#each currentServers as server, index (page * perPage + index)}
<ServerCard {server} />
{/each}
</Container> </Container>