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;
|
background: transparent;
|
||||||
color: white;
|
color: white;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
|
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: solid rgba(255, 255, 255, 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
import { createI18nStore } from 'svelte-i18next';
|
import { createI18nStore } from 'svelte-i18next';
|
||||||
|
import enTranslation from './locales/en.json';
|
||||||
|
|
||||||
i18next.init({
|
i18next.init({
|
||||||
lng: 'en'
|
lng: 'en',
|
||||||
|
resources: {
|
||||||
|
en: {
|
||||||
|
translation: enTranslation
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const i18n = createI18nStore(i18next);
|
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%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -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-radius: 0.5rem;
|
||||||
border: 1px solid white;
|
border: 1px solid white;
|
||||||
|
|
||||||
|
@ -27,11 +30,6 @@
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
main.sub {
|
|
||||||
height: 90vh;
|
|
||||||
width: 90vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-back {
|
.button-back {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 1rem;
|
top: 1rem;
|
||||||
|
|
|
@ -1,53 +1,243 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { version } from '../../../package.json';
|
import { version } from '../../../package.json';
|
||||||
import { i18n } from '$lib/i18n';
|
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>
|
</script>
|
||||||
|
|
||||||
|
<h1>Total Tech Test</h1>
|
||||||
|
|
||||||
<nav>
|
<nav>
|
||||||
<h1>Total Tech Test</h1>
|
<!-- svelte-ignore a11y-autofocus -->
|
||||||
|
<input type="search" placeholder={$i18n.t('Search')} bind:value={search} autofocus />
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<a href="card">
|
{#each superCategories as category}
|
||||||
<i class="ti ti-device-desktop"></i>
|
<button
|
||||||
{$i18n.t('Screen')}
|
on:click={() => setFilter(category.id)}
|
||||||
</a>
|
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
|
||||||
<a href="audio">
|
class="super"
|
||||||
<i class="ti ti-volume"></i>
|
>
|
||||||
{$i18n.t('Audio')}
|
<i class="ti {category.icon}"></i>
|
||||||
</a>
|
{$i18n.t(category.label)}
|
||||||
<a href="av-sync">
|
</button>
|
||||||
<i class="ti ti-time-duration-off"></i>
|
{/each}
|
||||||
{$i18n.t('AV Sync')}
|
<div class="separator"></div>
|
||||||
</a>
|
{#each categories as category}
|
||||||
<a href="keyboard">
|
<button
|
||||||
<i class="ti ti-keyboard"></i>
|
on:click={() => setFilter(category.id)}
|
||||||
{$i18n.t('Keyboard')}
|
class:active={!filteredCategories.length || filteredCategories.includes(category.id)}
|
||||||
</a>
|
>
|
||||||
<a href="mouse">
|
<i class="ti {category.icon}"></i>
|
||||||
<i class="ti ti-mouse"></i>
|
{$i18n.t(category.label)}
|
||||||
{$i18n.t('Mouse')}
|
</button>
|
||||||
</a>
|
{/each}
|
||||||
<a href="gamepad">
|
</div>
|
||||||
<i class="ti ti-device-gamepad"></i>
|
<div class="tests">
|
||||||
{$i18n.t('Gamepad')}
|
{#each nonEmptyCategories as category}
|
||||||
</a>
|
{#if tests.filter((test) => test.categories.includes(category.id)).length > 0}
|
||||||
<a href="camera">
|
<h2>{$i18n.t(category.label)}</h2>
|
||||||
<i class="ti ti-camera"></i>
|
{#each filteredTests.filter((test) => test.categories.includes(category.id) && filteredCategories.every( (f) => test.categories.includes(f) )) as test}
|
||||||
{$i18n.t('Camera')}
|
<div class="test" class:disabled={test.disabled}>
|
||||||
</a>
|
<a class="test" href={test.id}>
|
||||||
<a href="microphone" class="disabled">
|
<span class="label">
|
||||||
<i class="ti ti-microphone"></i>
|
<i class="ti {test.icon}"></i>
|
||||||
{$i18n.t('Microphone')}
|
{$i18n.t(test.label)}
|
||||||
</a>
|
</span>
|
||||||
<a href="sensors" class="disabled">
|
<span class="description">
|
||||||
<i class="ti ti-cpu-2"></i>
|
{$i18n.t(`tests.${test.id}.description`)}
|
||||||
{$i18n.t('Sensors')}
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p>
|
||||||
|
{$i18n.t('No tests found.')}
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
<footer><a href="https://git.thm.place/thm/test-card">testcard v{version}</a></footer>
|
<footer><a href="https://git.thm.place/thm/test-card">testcard v{version}</a></footer>
|
||||||
|
|
||||||
<style>
|
<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 {
|
h1 {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
|
@ -55,12 +245,36 @@
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2em;
|
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 {
|
& a {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
text-decoration: none;
|
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 {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
|
|
||||||
& a {
|
& a {
|
||||||
text-decoration: none;
|
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