feat: new nav with categories & search; add internet speed test and timer test
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
8e9d6cbe1e
commit
606cc0ca51
7 changed files with 448 additions and 46 deletions
|
@ -44,13 +44,18 @@ button, .button {
|
|||
}
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
input[type="number"], input[type="search"], input[type="text"] {
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: 1px solid white;
|
||||
border-radius: 4px;
|
||||
padding: 0.2em;
|
||||
|
||||
|
||||
&:focus {
|
||||
outline: solid rgba(255, 255, 255, 0.66);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import i18next from 'i18next';
|
||||
import { createI18nStore } from 'svelte-i18next';
|
||||
import enTranslation from './locales/en.json';
|
||||
|
||||
i18next.init({
|
||||
lng: 'en'
|
||||
lng: 'en',
|
||||
resources: {
|
||||
en: {
|
||||
translation: enTranslation
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const i18n = createI18nStore(i18next);
|
||||
|
|
49
src/lib/locales/en.json
Normal file
49
src/lib/locales/en.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"tests": {
|
||||
"audio": {
|
||||
"label": "Audio",
|
||||
"description": "Check your stereo channels or surround audio output, verify if your speakers are in phase."
|
||||
},
|
||||
"av-sync": {
|
||||
"label": "Audio/Video Sync",
|
||||
"description": "Check if your audio and video are in sync, and measure the delay."
|
||||
},
|
||||
"card": {
|
||||
"label": "Card",
|
||||
"description": "Test card for your display or projector, check colors, resolution and geometry."
|
||||
},
|
||||
"camera": {
|
||||
"label": "Camera",
|
||||
"description": "Check whether your webcam or capture device is working, its image quality, resolution and frame rate. Take a snapshot."
|
||||
},
|
||||
"gamepad": {
|
||||
"label": "Gamepad",
|
||||
"description": "Test your gamepad, check if it's working, all the buttons and joysticks, stick drift, dead zones and calibration."
|
||||
},
|
||||
"keyboard": {
|
||||
"label": "Keyboard",
|
||||
"description": "Check if all keys are working and what key codes they send."
|
||||
},
|
||||
"microphone": {
|
||||
"label": "Microphone",
|
||||
"description": "Check if your microphone is working, its quality, volume and noise."
|
||||
},
|
||||
"mouse": {
|
||||
"label": "Mouse",
|
||||
"description": "Check if your mouse or touch device works properly, if there are dead zones or jitter."
|
||||
},
|
||||
"sensors": {
|
||||
"label": "Sensors",
|
||||
"description": "See the output of your device's sensors, e.g. GPS, accelerometer, gyroscope, compass, etc."
|
||||
|
||||
},
|
||||
"internet": {
|
||||
"label": "Internet speed",
|
||||
"description": "Measure your internet speed, ping and jitter."
|
||||
},
|
||||
"timer": {
|
||||
"label": "High resolution timer",
|
||||
"description": "Display a microsecond resolution timer on screen, useful for measuring video pipeline latency."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,10 @@
|
|||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
height: 90vh;
|
||||
width: 90vw;
|
||||
|
||||
background: rgba(0, 0, 0, 0.85);
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid white;
|
||||
|
||||
|
@ -27,11 +30,6 @@
|
|||
flex-direction: column;
|
||||
}
|
||||
|
||||
main.sub {
|
||||
height: 90vh;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.button-back {
|
||||
position: absolute;
|
||||
top: 1rem;
|
||||
|
|
|
@ -1,53 +1,243 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { version } from '../../../package.json';
|
||||
import { i18n } from '$lib/i18n';
|
||||
import type { Snapshot } from '@sveltejs/kit';
|
||||
|
||||
let search = '';
|
||||
|
||||
type Entry = {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
};
|
||||
|
||||
type Test = Entry & {
|
||||
categories: Array<Category>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type Category = (typeof categories)[number]['id'] | (typeof superCategories)[number]['id'];
|
||||
|
||||
let superCategories = [
|
||||
{
|
||||
id: 'inputs',
|
||||
label: 'Inputs',
|
||||
icon: 'ti-arrow-down-to-arc'
|
||||
},
|
||||
{
|
||||
id: 'outputs',
|
||||
label: 'Outputs',
|
||||
icon: 'ti-arrow-down-from-arc'
|
||||
}
|
||||
] as const satisfies Entry[];
|
||||
|
||||
let categories = [
|
||||
{
|
||||
id: 'audio',
|
||||
label: 'Audio',
|
||||
icon: 'ti-volume'
|
||||
},
|
||||
{
|
||||
id: 'video',
|
||||
label: 'Video',
|
||||
icon: 'ti-video'
|
||||
},
|
||||
{
|
||||
id: 'control',
|
||||
label: 'Control',
|
||||
icon: 'ti-hand-finger'
|
||||
},
|
||||
{
|
||||
id: 'misc',
|
||||
label: 'Miscellaneous',
|
||||
icon: 'ti-circle-plus'
|
||||
}
|
||||
] as const satisfies Entry[];
|
||||
|
||||
const tests: Test[] = [
|
||||
{
|
||||
id: 'card',
|
||||
label: 'Test Card',
|
||||
icon: 'ti-device-desktop',
|
||||
categories: ['outputs', 'video']
|
||||
},
|
||||
{
|
||||
id: 'audio',
|
||||
label: 'Audio',
|
||||
icon: 'ti-volume',
|
||||
categories: ['outputs', 'audio']
|
||||
},
|
||||
{
|
||||
id: 'av-sync',
|
||||
label: 'AV Sync',
|
||||
icon: 'ti-time-duration-off',
|
||||
categories: ['outputs', 'video', 'audio']
|
||||
},
|
||||
{
|
||||
id: 'keyboard',
|
||||
label: 'Keyboard',
|
||||
icon: 'ti-keyboard',
|
||||
categories: ['inputs', 'control']
|
||||
},
|
||||
{
|
||||
id: 'mouse',
|
||||
label: 'Mouse',
|
||||
icon: 'ti-mouse',
|
||||
categories: ['inputs', 'control']
|
||||
},
|
||||
{
|
||||
id: 'gamepad',
|
||||
label: 'Gamepad',
|
||||
icon: 'ti-device-gamepad',
|
||||
categories: ['inputs', 'control']
|
||||
},
|
||||
{
|
||||
id: 'camera',
|
||||
label: 'Camera',
|
||||
icon: 'ti-camera',
|
||||
categories: ['inputs', 'video']
|
||||
},
|
||||
{
|
||||
id: 'microphone',
|
||||
label: 'Microphone',
|
||||
icon: 'ti-microphone',
|
||||
categories: ['inputs', 'audio'],
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
id: 'sensors',
|
||||
label: 'Sensors',
|
||||
icon: 'ti-cpu-2',
|
||||
categories: ['inputs', 'misc'],
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
id: 'internet',
|
||||
label: 'Internet speed',
|
||||
icon: 'ti-world',
|
||||
categories: ['inputs', 'outputs', 'misc']
|
||||
},
|
||||
{
|
||||
id: 'timer',
|
||||
label: 'High resolution timer',
|
||||
icon: 'ti-alarm',
|
||||
categories: ['video']
|
||||
}
|
||||
];
|
||||
|
||||
let filteredTests: Test[] = tests;
|
||||
let filteredCategories: Category[] = [];
|
||||
|
||||
function doSearch(search: string) {
|
||||
filteredTests = tests.filter((test) => {
|
||||
if (!search) return true;
|
||||
|
||||
const searchValue = search.toLocaleLowerCase();
|
||||
return (
|
||||
test.label.includes(searchValue) ||
|
||||
test.id.includes(searchValue) ||
|
||||
$i18n.t(`tests.${test.id}.description`).includes(searchValue)
|
||||
);
|
||||
});
|
||||
}
|
||||
$: doSearch(search);
|
||||
|
||||
function setFilter(category: Category) {
|
||||
if (filteredCategories.includes(category)) {
|
||||
filteredCategories = filteredCategories.filter((c) => c !== category);
|
||||
} else {
|
||||
filteredCategories = [...filteredCategories, category];
|
||||
}
|
||||
}
|
||||
|
||||
$: nonEmptyCategories = categories.filter((category) => {
|
||||
const categoryTests = filteredTests.filter((test) => test.categories.includes(category.id));
|
||||
return categoryTests.some(
|
||||
(test) =>
|
||||
!filteredCategories.length || filteredCategories.every((f) => test.categories.includes(f))
|
||||
);
|
||||
});
|
||||
|
||||
export const snapshot: Snapshot<string> = {
|
||||
capture: () => JSON.stringify({ filtered: filteredCategories, search }),
|
||||
restore: (value) => {
|
||||
const { filtered: restoredFiltered, search: restoredSearch } = JSON.parse(value);
|
||||
filteredCategories = restoredFiltered;
|
||||
search = restoredSearch;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<nav>
|
||||
<h1>Total Tech Test</h1>
|
||||
|
||||
<nav>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input type="search" placeholder={$i18n.t('Search')} bind:value={search} autofocus />
|
||||
|
||||
<div class="options">
|
||||
<a href="card">
|
||||
<i class="ti ti-device-desktop"></i>
|
||||
{$i18n.t('Screen')}
|
||||
</a>
|
||||
<a href="audio">
|
||||
<i class="ti ti-volume"></i>
|
||||
{$i18n.t('Audio')}
|
||||
</a>
|
||||
<a href="av-sync">
|
||||
<i class="ti ti-time-duration-off"></i>
|
||||
{$i18n.t('AV Sync')}
|
||||
</a>
|
||||
<a href="keyboard">
|
||||
<i class="ti ti-keyboard"></i>
|
||||
{$i18n.t('Keyboard')}
|
||||
</a>
|
||||
<a href="mouse">
|
||||
<i class="ti ti-mouse"></i>
|
||||
{$i18n.t('Mouse')}
|
||||
</a>
|
||||
<a href="gamepad">
|
||||
<i class="ti ti-device-gamepad"></i>
|
||||
{$i18n.t('Gamepad')}
|
||||
</a>
|
||||
<a href="camera">
|
||||
<i class="ti ti-camera"></i>
|
||||
{$i18n.t('Camera')}
|
||||
</a>
|
||||
<a href="microphone" class="disabled">
|
||||
<i class="ti ti-microphone"></i>
|
||||
{$i18n.t('Microphone')}
|
||||
</a>
|
||||
<a href="sensors" class="disabled">
|
||||
<i class="ti ti-cpu-2"></i>
|
||||
{$i18n.t('Sensors')}
|
||||
{#each superCategories as category}
|
||||
<button
|
||||
on:click={() => setFilter(category.id)}
|
||||
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
|
||||
class="super"
|
||||
>
|
||||
<i class="ti {category.icon}"></i>
|
||||
{$i18n.t(category.label)}
|
||||
</button>
|
||||
{/each}
|
||||
<div class="separator"></div>
|
||||
{#each categories as category}
|
||||
<button
|
||||
on:click={() => setFilter(category.id)}
|
||||
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
|
||||
>
|
||||
<i class="ti {category.icon}"></i>
|
||||
{$i18n.t(category.label)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="tests">
|
||||
{#each nonEmptyCategories as category}
|
||||
{#if tests.filter((test) => test.categories.includes(category.id)).length > 0}
|
||||
<h2>{$i18n.t(category.label)}</h2>
|
||||
{#each filteredTests.filter((test) => test.categories.includes(category.id) && filteredCategories.every( (f) => test.categories.includes(f) )) as test}
|
||||
<div class="test" class:disabled={test.disabled}>
|
||||
<a class="test" href={test.id}>
|
||||
<span class="label">
|
||||
<i class="ti {test.icon}"></i>
|
||||
{$i18n.t(test.label)}
|
||||
</span>
|
||||
<span class="description">
|
||||
{$i18n.t(`tests.${test.id}.description`)}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{:else}
|
||||
<p>
|
||||
{$i18n.t('No tests found.')}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
</nav>
|
||||
<footer><a href="https://git.thm.place/thm/test-card">testcard v{version}</a></footer>
|
||||
|
||||
<style>
|
||||
nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
flex-grow: 1;
|
||||
padding: 0 4rem;
|
||||
overflow: auto;
|
||||
|
||||
& input[type='search'] {
|
||||
padding: 0.5em;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
font-size: 3rem;
|
||||
|
@ -55,12 +245,36 @@
|
|||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
gap: 2em;
|
||||
|
||||
& button {
|
||||
border: none;
|
||||
background: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
opacity: 0.66;
|
||||
|
||||
&.active {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
& .separator {
|
||||
border-left: 1px solid currentColor;
|
||||
height: 3rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
& a {
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
|
@ -78,10 +292,35 @@
|
|||
}
|
||||
}
|
||||
|
||||
.test {
|
||||
display: flex;
|
||||
gap: 0.5em;
|
||||
margin-bottom: 0.25em;
|
||||
|
||||
& .label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
& .description {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
text-align: center;
|
||||
opacity: 0.6;
|
||||
margin-top: 1rem;
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
31
src/routes/(pages)/internet/+page.svelte
Normal file
31
src/routes/(pages)/internet/+page.svelte
Normal file
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { i18n } from '$lib/i18n';
|
||||
</script>
|
||||
|
||||
<h2><i class="ti ti-world"></i> {$i18n.t('Internet speed')}</h2>
|
||||
|
||||
<div class="test">
|
||||
<iframe src="//openspeedtest.com/speedtest" title="OpenSpeedTest Embed"></iframe>
|
||||
</div>
|
||||
<div class="attribution">
|
||||
Provided by <a href="https://openspeedtest.com">OpenSpeedtest.com</a>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
border: none;
|
||||
flex-grow: 1;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.test {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.attribution {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
74
src/routes/(pages)/timer/+page.svelte
Normal file
74
src/routes/(pages)/timer/+page.svelte
Normal file
|
@ -0,0 +1,74 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { i18n } from '$lib/i18n';
|
||||
|
||||
let time = 0;
|
||||
let fps = 0;
|
||||
let start = 0;
|
||||
let displayFps = '?';
|
||||
let fpsInterval: NodeJS.Timeout | undefined;
|
||||
|
||||
const times: number[] = [];
|
||||
function refreshLoop() {
|
||||
const now = performance.now();
|
||||
time = Math.floor(now - start);
|
||||
while (times.length > 0 && times[0] <= now - 1000) {
|
||||
times.shift();
|
||||
}
|
||||
times.push(now);
|
||||
fps = times.length;
|
||||
window.requestAnimationFrame(refreshLoop);
|
||||
}
|
||||
|
||||
function restart() {
|
||||
displayFps = '?';
|
||||
times.length = 0;
|
||||
start = performance.now();
|
||||
clearInterval(fpsInterval);
|
||||
fpsInterval = setInterval(() => {
|
||||
displayFps = fps.toString();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
refreshLoop();
|
||||
restart();
|
||||
});
|
||||
</script>
|
||||
|
||||
<h2><i class="ti ti-alarm"></i> {$i18n.t('High resolution timer')}</h2>
|
||||
<div class="display">
|
||||
<div class="time">{time}</div>
|
||||
<div class="fps">{displayFps} {$i18n.t('FPS')}</div>
|
||||
</div>
|
||||
<button on:click={restart}>{$i18n.t('Restart')}</button>
|
||||
|
||||
<style>
|
||||
div,
|
||||
button {
|
||||
text-align: center;
|
||||
margin-bottom: 1rem;
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.display {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12rem;
|
||||
}
|
||||
|
||||
.fps {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
button {
|
||||
align-self: center;
|
||||
font-size: 2rem;
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue