feat: dotykové ovládání miniher na mobilu a tabletu
CI / Generate TypeScript types (push) Successful in 13s
CI / Server unit tests (push) Successful in 27s
CI / Build server (push) Successful in 33s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m54s
CI / Build and push Docker image (push) Successful in 46s
CI / Notify (push) Successful in 2s
CI / Generate TypeScript types (push) Successful in 13s
CI / Server unit tests (push) Successful in 27s
CI / Build server (push) Successful in 33s
CI / Build client (push) Successful in 42s
CI / Playwright E2E tests (push) Successful in 1m54s
CI / Build and push Docker image (push) Successful in 46s
CI / Notify (push) Successful in 2s
This commit is contained in:
18 files changed
+503
-63
No files matched your search
@@ -67,15 +67,20 @@ function printHelp() {
|
||||
console.table(MINIGAMES.map(g => ({ kód: g.code, hra: `${gameIcon(g.id)} ${g.name}`, popis: g.description })));
|
||||
}
|
||||
|
||||
/** Malá písmena bez diakritiky — česká klávesnice na mobilu může "kresleni" opravit na "Kreslení". */
|
||||
const normalizeCode = (text: string) => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').trim();
|
||||
|
||||
/**
|
||||
* Skrytý vstup do miniher: 3× rychle kliknout na logo a pak napsat kód hry,
|
||||
* případně "help" pro výpis her do konzole prohlížeče.
|
||||
* Jedno či dvě kliknutí se chovají jako běžný odkaz (jen s malým zpožděním).
|
||||
* Na dotykových zařízeních se kód píše do skrytého pole (`inputRef`), které otevře softwarovou klávesnici.
|
||||
*/
|
||||
export function useMinigameSecret(href: string) {
|
||||
const [armed, setArmed] = useState(false);
|
||||
const clicks = useRef(0);
|
||||
const timer = useRef<number | undefined>(undefined);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onLogoClick = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
// Otevření v novém tabu apod. necháme prohlížeči
|
||||
@@ -85,6 +90,12 @@ export function useMinigameSecret(href: string) {
|
||||
clicks.current++;
|
||||
if (clicks.current >= SECRET_CLICKS) {
|
||||
clicks.current = 0;
|
||||
// Fokus musí proběhnout přímo v obsluze kliknutí, jinak iOS klávesnici neotevře
|
||||
const input = inputRef.current;
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.focus({ preventScroll: true });
|
||||
}
|
||||
setArmed(true);
|
||||
return;
|
||||
}
|
||||
@@ -96,32 +107,44 @@ export function useMinigameSecret(href: string) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!armed) return;
|
||||
const input = inputRef.current;
|
||||
const maxLength = Math.max(HELP_CODE.length, ...MINIGAMES.map(g => g.code.length));
|
||||
let typed = '';
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target?.closest('input, textarea, select, [contenteditable="true"]') || e.key.length !== 1) return;
|
||||
typed = (typed + e.key.toLowerCase()).slice(-maxLength);
|
||||
if (typed.endsWith(HELP_CODE)) {
|
||||
const check = (text: string) => {
|
||||
if (text.endsWith(HELP_CODE)) {
|
||||
setArmed(false);
|
||||
printHelp();
|
||||
return;
|
||||
}
|
||||
const game = MINIGAMES.find(g => typed.endsWith(g.code));
|
||||
const game = MINIGAMES.find(g => text.endsWith(g.code));
|
||||
if (game) {
|
||||
setArmed(false);
|
||||
game.open();
|
||||
}
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target?.closest('input, textarea, select, [contenteditable="true"]') || e.key.length !== 1) return;
|
||||
typed = (typed + normalizeCode(e.key)).slice(-maxLength);
|
||||
check(typed);
|
||||
};
|
||||
// Softwarová klávesnice (Android) posílá v keydown jen "Unidentified", proto se čte celý text pole
|
||||
const onInput = () => check(normalizeCode(input?.value ?? ''));
|
||||
const disarm = window.setTimeout(() => setArmed(false), SECRET_ARM_MS);
|
||||
window.addEventListener('keydown', onKey);
|
||||
input?.addEventListener('input', onInput);
|
||||
return () => {
|
||||
window.clearTimeout(disarm);
|
||||
window.removeEventListener('keydown', onKey);
|
||||
input?.removeEventListener('input', onInput);
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.blur();
|
||||
}
|
||||
};
|
||||
}, [armed]);
|
||||
|
||||
useEffect(() => () => window.clearTimeout(timer.current), []);
|
||||
|
||||
return { armed, onLogoClick };
|
||||
return { armed, onLogoClick, inputRef };
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/** Libovolné dotykové ovládání — iPad se hlásí jako Mac, takže se nedá spoléhat na user agent. */
|
||||
const QUERY = '(any-pointer: coarse)';
|
||||
|
||||
/** Zda má zařízení dotykový displej (podle toho minihry ukazují dotyková tlačítka a nápovědu). */
|
||||
export function useTouchDevice() {
|
||||
const [touch, setTouch] = useState(() => window.matchMedia(QUERY).matches);
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia(QUERY);
|
||||
const onChange = () => setTouch(media.matches);
|
||||
media.addEventListener('change', onChange);
|
||||
return () => media.removeEventListener('change', onChange);
|
||||
}, []);
|
||||
return touch;
|
||||
}
|
||||
Reference in new issue
Block a user