From e7478d3c64889ab35d91fe69fd06969e4aeb71bd Mon Sep 17 00:00:00 2001 From: yasder5 <102179445+Yasder5@users.noreply.github.com> Date: Sun, 4 Oct 2026 14:22:40 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Navigation=20manette=20:=20focus=20spatial,?= =?UTF-8?q?=20Gamepad=20API=20Xbox/PlayStation,=20r=C3=A9p=C3=A9tition,=20?= =?UTF-8?q?glyphes=20adapt=C3=A9s,=20vibration,=20i18n=20FR/EN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/i18n/en.ts | 14 ++++ src/renderer/src/i18n/fr.ts | 12 ++++ src/renderer/src/i18n/i18n.tsx | 18 ++++++ src/renderer/src/nav/glyph-map.ts | 10 +++ src/renderer/src/nav/glyphs.tsx | 22 +++++++ src/renderer/src/nav/input.ts | 103 ++++++++++++++++++++++++++++++ src/renderer/src/nav/nav.tsx | 94 +++++++++++++++++++++++++++ src/renderer/src/nav/sounds.ts | 38 +++++++++++ src/renderer/src/nav/spatial.ts | 27 ++++++++ tests/i18n.test.ts | 13 ++++ tests/input.test.ts | 38 +++++++++++ tests/spatial.test.ts | 32 ++++++++++ 12 files changed, 421 insertions(+) create mode 100644 src/renderer/src/i18n/en.ts create mode 100644 src/renderer/src/i18n/fr.ts create mode 100644 src/renderer/src/i18n/i18n.tsx create mode 100644 src/renderer/src/nav/glyph-map.ts create mode 100644 src/renderer/src/nav/glyphs.tsx create mode 100644 src/renderer/src/nav/input.ts create mode 100644 src/renderer/src/nav/nav.tsx create mode 100644 src/renderer/src/nav/sounds.ts create mode 100644 src/renderer/src/nav/spatial.ts create mode 100644 tests/i18n.test.ts create mode 100644 tests/input.test.ts create mode 100644 tests/spatial.test.ts diff --git a/src/renderer/src/i18n/en.ts b/src/renderer/src/i18n/en.ts new file mode 100644 index 0000000..0a76169 --- /dev/null +++ b/src/renderer/src/i18n/en.ts @@ -0,0 +1,14 @@ +import type { fr } from './fr' + +export const en: Record = { + 'hint.select': 'Select', + 'hint.back': 'Back', + 'hint.ok': 'Confirm', + 'hint.delete': 'Delete', + 'hint.space': 'Space', + 'hint.shift': 'Shift', + 'kb.title': 'Input', + 'kb.ok': 'OK', + 'kb.space': 'Space', + 'kb.delete': 'Delete' +} diff --git a/src/renderer/src/i18n/fr.ts b/src/renderer/src/i18n/fr.ts new file mode 100644 index 0000000..940abf2 --- /dev/null +++ b/src/renderer/src/i18n/fr.ts @@ -0,0 +1,12 @@ +export const fr = { + 'hint.select': 'Choisir', + 'hint.back': 'Retour', + 'hint.ok': 'Valider', + 'hint.delete': 'Effacer', + 'hint.space': 'Espace', + 'hint.shift': 'Majuscules', + 'kb.title': 'Saisie', + 'kb.ok': 'OK', + 'kb.space': 'Espace', + 'kb.delete': 'Effacer' +} diff --git a/src/renderer/src/i18n/i18n.tsx b/src/renderer/src/i18n/i18n.tsx new file mode 100644 index 0000000..622dcd1 --- /dev/null +++ b/src/renderer/src/i18n/i18n.tsx @@ -0,0 +1,18 @@ +/* Traductions : FR par défaut, EN. Clés plates, variables {nom}. */ +import { useCallback } from 'react' +import { useSettings } from '../state/settings' +import { en } from './en' +import { fr } from './fr' + +export type Key = keyof typeof fr +export const dicts = { fr, en } satisfies Record> + +export function translate(lang: 'fr' | 'en', key: string, vars?: Record): string { + const s = (dicts[lang] as Record)[key] ?? (fr as Record)[key] ?? key + return vars ? s.replace(/\{(\w+)\}/g, (_, k) => String(vars[k] ?? '')) : s +} + +export function useT() { + const { settings } = useSettings() + return useCallback((key: string, vars?: Record) => translate(settings.language, key, vars), [settings.language]) +} diff --git a/src/renderer/src/nav/glyph-map.ts b/src/renderer/src/nav/glyph-map.ts new file mode 100644 index 0000000..8fff9ad --- /dev/null +++ b/src/renderer/src/nav/glyph-map.ts @@ -0,0 +1,10 @@ +/* Libellés des boutons selon la manette (pur, testé). */ +import type { Action, InputKind } from './input' + +const XBOX: Partial> = { accept: 'A', back: 'B', x: 'X', y: 'Y', lb: 'LB', rb: 'RB', lt: 'LT', rt: 'RT', menu: '☰', view: '⧉' } +const PS: Partial> = { accept: '✕', back: '○', x: '□', y: '△', lb: 'L1', rb: 'R1', lt: 'L2', rt: 'R2', menu: 'OPTIONS', view: 'SHARE' } +const KEY: Partial> = { accept: 'Entrée', back: 'Échap', x: 'X', y: 'Y', lb: 'Q', rb: 'E', lt: 'PgPréc', rt: 'PgSuiv', menu: 'F1', view: 'Tab' } + +export function glyph(a: Action, kind: InputKind): string { + return (kind === 'ps' ? PS : kind === 'xbox' ? XBOX : KEY)[a] ?? a +} diff --git a/src/renderer/src/nav/glyphs.tsx b/src/renderer/src/nav/glyphs.tsx new file mode 100644 index 0000000..3c118ee --- /dev/null +++ b/src/renderer/src/nav/glyphs.tsx @@ -0,0 +1,22 @@ +/* Boutons affichés selon la manette détectée (A/B Xbox, Croix/Rond PlayStation, touches clavier). */ +import { glyph } from './glyph-map' +import type { Action } from './input' +import { useNavCtx } from './nav' +import { useT } from '../i18n/i18n' + +export function Glyph({ action }: { action: Action }) { + const { kind } = useNavCtx() + return {glyph(action, kind)} +} + +/* Barre d'aide en bas d'écran : ce que fait chaque bouton ici */ +export function HintBar({ items }: { items: [Action, string][] }) { + const t = useT() + return ( +
+ {items.map(([a, label]) => ( + {t(label)} + ))} +
+ ) +} diff --git a/src/renderer/src/nav/input.ts b/src/renderer/src/nav/input.ts new file mode 100644 index 0000000..bf82be8 --- /dev/null +++ b/src/renderer/src/nav/input.ts @@ -0,0 +1,103 @@ +/* Entrées : manette (Gamepad API), clavier, souris, ramenées à des actions. Répétition quand une direction est tenue. */ + +export type Action = 'up' | 'down' | 'left' | 'right' | 'accept' | 'back' | 'x' | 'y' | 'lb' | 'rb' | 'lt' | 'rt' | 'menu' | 'view' +export type InputKind = 'xbox' | 'ps' | 'keyboard' | 'mouse' + +const REPEAT_DELAY = 380, REPEAT_RATE = 80 // ms +const DIRS: Action[] = ['up', 'down', 'left', 'right'] + +/* Boutons de la disposition standard (W3C) */ +const BUTTONS: [number, Action][] = [ + [0, 'accept'], [1, 'back'], [2, 'x'], [3, 'y'], [4, 'lb'], [5, 'rb'], [6, 'lt'], [7, 'rt'], [8, 'view'], [9, 'menu'], + [12, 'up'], [13, 'down'], [14, 'left'], [15, 'right'] +] + +const KEYS: Record = { + ArrowUp: 'up', ArrowDown: 'down', ArrowLeft: 'left', ArrowRight: 'right', + Enter: 'accept', ' ': 'accept', Escape: 'back', Backspace: 'back', + KeyX: 'x', KeyY: 'y', KeyQ: 'lb', KeyE: 'rb', PageUp: 'lt', PageDown: 'rt', F1: 'menu', Tab: 'view' +} + +export function padKind(id: string): InputKind { + return /054c|playstation|dualsense|dualshock|wireless controller/i.test(id) ? 'ps' : 'xbox' +} + +/* Gestion de la répétition pour un bouton tenu ; renvoie true si l'action doit partir à cet instant */ +export class Repeater { + private since = new Map() + private last = new Map() + step(a: Action, down: boolean, now: number): boolean { + if (!down) { this.since.delete(a); return false } + const s = this.since.get(a) + if (s === undefined) { this.since.set(a, now); this.last.set(a, now); return true } + if (!DIRS.includes(a)) return false // seules les directions se répètent + if (now - s >= REPEAT_DELAY && now - (this.last.get(a) ?? 0) >= REPEAT_RATE) { this.last.set(a, now); return true } + return false + } +} + +/* Lecture des états d'une manette en actions enfoncées */ +export function padActions(gp: Pick, dead = 0.5): Set { + const on = new Set() + for (const [i, a] of BUTTONS) if (gp.buttons[i]?.pressed) on.add(a) + const [x = 0, y = 0] = gp.axes + if (x < -dead) on.add('left') + if (x > dead) on.add('right') + if (y < -dead) on.add('up') + if (y > dead) on.add('down') + return on +} + +export interface InputHandlers { + action: (a: Action) => void + kind: (k: InputKind) => void + guideHeld?: () => void +} + +/* Démarre l'écoute ; renvoie une fonction d'arrêt */ +export function startInput(h: InputHandlers): () => void { + const rep = new Repeater() + let raf = 0, kind: InputKind | null = null, guideSince = 0 + const setKind = (k: InputKind) => { if (k !== kind) { kind = k; h.kind(k) } } + + const loop = () => { + raf = requestAnimationFrame(loop) + if (!document.hasFocus()) return + const now = performance.now() + const pads = navigator.getGamepads?.() ?? [] + const on = new Set() + let guide = false + for (const gp of pads) { + if (!gp || !gp.connected) continue + const acts = padActions(gp) + if (acts.size) setKind(padKind(gp.id)) + acts.forEach((a) => on.add(a)) + if (gp.buttons[16]?.pressed) guide = true + } + for (const [, a] of BUTTONS) if (rep.step(a, on.has(a), now)) h.action(a) + // bouton Guide / PS tenu 0,8 s (quand Chromium le remonte ; sinon le processus principal s'en charge via XInput) + if (guide) { if (!guideSince) guideSince = now; else if (now - guideSince > 800) { guideSince = Infinity; h.guideHeld?.() } } else guideSince = 0 + } + raf = requestAnimationFrame(loop) + + const onKey = (e: KeyboardEvent) => { + const a = KEYS[e.key] ?? KEYS[e.code] + const t = e.target as HTMLElement + if (t?.tagName === 'INPUT' && !['ArrowUp', 'ArrowDown', 'Enter', 'Escape', 'Tab'].includes(e.key)) return // saisie au clavier physique + if (!a) return + e.preventDefault() + setKind('keyboard') + h.action(a) // le clavier répète déjà tout seul + } + const onMouse = () => setKind('mouse') + addEventListener('keydown', onKey) + addEventListener('mousemove', onMouse) + return () => { cancelAnimationFrame(raf); removeEventListener('keydown', onKey); removeEventListener('mousemove', onMouse) } +} + +export function rumble(): void { + for (const gp of navigator.getGamepads?.() ?? []) { + const act = (gp as Gamepad & { vibrationActuator?: { playEffect: (t: string, p: object) => Promise } })?.vibrationActuator + act?.playEffect('dual-rumble', { duration: 35, weakMagnitude: 0.25, strongMagnitude: 0 }).catch(() => {}) + } +} diff --git a/src/renderer/src/nav/nav.tsx b/src/renderer/src/nav/nav.tsx new file mode 100644 index 0000000..423ae06 --- /dev/null +++ b/src/renderer/src/nav/nav.tsx @@ -0,0 +1,94 @@ +/* Gestionnaire de navigation : focus spatial dans la portée active (écran ou fenêtre modale), actions manette/clavier, sons. */ +import { createContext, useCallback, useContext, useEffect, useRef, useState, type ReactNode } from 'react' +import type { SoundName } from '@shared/theme' +import { useSettings } from '../state/settings' +import { startInput, rumble, type Action, type InputKind } from './input' +import { play } from './sounds' +import { pickNext, type Dir } from './spatial' + +type Handler = (a: Action) => boolean | void // true = action consommée + +interface Ctx { + kind: InputKind + focus: (el: Element | null) => void + push: (h: { current: Handler }) => () => void + sound: (s: SoundName) => void + current: () => HTMLElement | null +} + +const NavCtx = createContext(null!) +const SEL = '[data-nav]:not([disabled])' + +/* Portée active : la dernière fenêtre modale ouverte, sinon tout le document */ +const scope = (): ParentNode => { + const s = document.querySelectorAll('[data-nav-scope]') + return s.length ? s[s.length - 1] : document +} +const visible = (el: Element) => { const r = el.getBoundingClientRect(); return r.width > 0 && r.height > 0 } + +export function NavProvider({ children, onGuide }: { children: ReactNode; onGuide?: () => void }) { + const [kind, setKind] = useState('mouse') + const cur = useRef(null) + const stack = useRef<{ current: Handler }[]>([]) + const { settings } = useSettings() + const vib = useRef(settings.vibration) + vib.current = settings.vibration + + const focus = useCallback((el: Element | null) => { + if (!(el instanceof HTMLElement) || el === cur.current) return + cur.current?.classList.remove('is-focused') + cur.current = el + el.classList.add('is-focused') + el.focus({ preventScroll: true }) + el.scrollIntoView({ block: el.dataset.navScroll === 'center' ? 'center' : 'nearest', inline: 'nearest', behavior: 'smooth' }) + }, []) + + /* Remet le focus sur un élément valide (écran changé, élément supprimé, fenêtre fermée) */ + const refocus = useCallback(() => { + const sc = scope(), c = cur.current + if (c && c.isConnected && (sc === document || (sc as Element).contains(c)) && visible(c)) return + const auto = sc.querySelector('[data-nav-autofocus]' ) ?? [...sc.querySelectorAll(SEL)].find(visible) ?? null + if (c && !c.isConnected) cur.current = null + if (auto) focus(auto) + }, [focus]) + + const move = useCallback((dir: Dir) => { + const sc = scope(), from = cur.current + const els = [...sc.querySelectorAll(SEL)].filter(visible) + if (!from || !from.isConnected) { if (els[0]) focus(els[0]); return } + // un groupe (data-nav-group) garde le focus dans sa ligne pour gauche/droite + const r = from.getBoundingClientRect(), box = { x: r.left, y: r.top, w: r.width, h: r.height } + const next = pickNext(box, els.filter((e) => e !== from).map((el) => { const b = el.getBoundingClientRect(); return { el, box: { x: b.left, y: b.top, w: b.width, h: b.height } } }), dir) + if (next) { focus(next.el); play('move'); if (vib.current) rumble() } + }, [focus]) + + useEffect(() => { + const stop = startInput({ + kind: (k) => { setKind(k); document.documentElement.dataset.input = k === 'mouse' ? 'mouse' : k === 'keyboard' ? 'keyboard' : 'pad' }, + action: (a) => { + for (let i = stack.current.length - 1; i >= 0; i--) if (stack.current[i].current(a)) return + if (a === 'up' || a === 'down' || a === 'left' || a === 'right') move(a) + else if (a === 'accept' && cur.current) { play('select'); cur.current.click() } + }, + guideHeld: onGuide + }) + const over = (e: PointerEvent) => { const t = (e.target as Element).closest?.(SEL); if (t) focus(t) } + const mo = new MutationObserver(() => requestAnimationFrame(refocus)) + mo.observe(document.body, { childList: true, subtree: true }) + addEventListener('pointerover', over) + return () => { stop(); mo.disconnect(); removeEventListener('pointerover', over) } + }, [move, focus, refocus, onGuide]) + + const push = useCallback((h: { current: Handler }) => { stack.current.push(h); return () => { stack.current = stack.current.filter((x) => x !== h) } }, []) + return cur.current }}>{children} +} + +export const useNavCtx = () => useContext(NavCtx) + +/* Intercepte les actions tant que le composant est affiché (le plus récent passe en premier) */ +export function useNav(handler: Handler, active = true): void { + const { push } = useNavCtx() + const ref = useRef(handler) + ref.current = handler + useEffect(() => (active ? push(ref) : undefined), [push, active]) +} diff --git a/src/renderer/src/nav/sounds.ts b/src/renderer/src/nav/sounds.ts new file mode 100644 index 0000000..432fe9f --- /dev/null +++ b/src/renderer/src/nav/sounds.ts @@ -0,0 +1,38 @@ +/* Sons de navigation du thème (Web Audio : latence faible, plusieurs sons superposables). */ +import type { SoundName, ThemeManifest } from '@shared/theme' + +let ctx: AudioContext | null = null +const buffers = new Map() +let volume = 0.6, token = 0 + +export async function loadSounds(base: string, sounds: ThemeManifest['sounds']): Promise { + const my = ++token + buffers.clear() + ctx ??= new AudioContext() + await Promise.all( + Object.entries(sounds).map(async ([name, rel]) => { + try { + const data = await (await fetch(base + rel)).arrayBuffer() + const buf = await ctx!.decodeAudioData(data) + if (my === token) buffers.set(name as SoundName, buf) // un autre thème a pu être choisi entre-temps + } catch { + /* son manquant ou illisible : silence */ + } + }) + ) +} + +export function setVolume(v: number): void { + volume = v +} + +export function play(name: SoundName): void { + const buf = buffers.get(name) + if (!ctx || !buf || volume <= 0) return + if (ctx.state === 'suspended') ctx.resume() + const src = ctx.createBufferSource(), gain = ctx.createGain() + gain.gain.value = volume + src.buffer = buf + src.connect(gain).connect(ctx.destination) + src.start() +} diff --git a/src/renderer/src/nav/spatial.ts b/src/renderer/src/nav/spatial.ts new file mode 100644 index 0000000..b7f0f65 --- /dev/null +++ b/src/renderer/src/nav/spatial.ts @@ -0,0 +1,27 @@ +/* Focus spatial : choisit l'élément le plus naturel dans une direction. Pur (rectangles en entrée) pour être testé. */ + +export type Dir = 'up' | 'down' | 'left' | 'right' +export interface Box { x: number; y: number; w: number; h: number } + +/* Distance « ressentie » : on avance dans la direction, en pénalisant fort le décalage latéral. + Un candidat qui chevauche l'axe (même ligne / même colonne) passe devant un candidat en diagonale. */ +export function pickNext(from: Box, items: T[], dir: Dir): T | null { + const horiz = dir === 'left' || dir === 'right', sign = dir === 'right' || dir === 'down' ? 1 : -1 + let best: T | null = null, bestScore = Infinity + for (const it of items) { + const b = it.box + if (b === from) continue + // avance : bord de départ -> bord d'arrivée + const gap = horiz ? (sign > 0 ? b.x - (from.x + from.w) : from.x - (b.x + b.w)) : sign > 0 ? b.y - (from.y + from.h) : from.y - (b.y + b.h) + const centerAdvance = horiz ? (b.x + b.w / 2 - (from.x + from.w / 2)) * sign : (b.y + b.h / 2 - (from.y + from.h / 2)) * sign + if (centerAdvance <= 1 || gap < -Math.min(horiz ? from.w : from.h, horiz ? b.w : b.h) / 2) continue + // décalage latéral : 0 si les projections se chevauchent + const [a0, a1, b0, b1] = horiz ? [from.y, from.y + from.h, b.y, b.y + b.h] : [from.x, from.x + from.w, b.x, b.x + b.w] + const overlap = Math.min(a1, b1) - Math.max(a0, b0) + const lateral = overlap > 0 ? 0 : Math.max(b0 - a1, a0 - b1) + const lateralCenter = Math.abs(horiz ? b.y + b.h / 2 - (from.y + from.h / 2) : b.x + b.w / 2 - (from.x + from.w / 2)) + const score = Math.max(gap, 0) + lateral * 4 + lateralCenter * 0.1 + (overlap > 0 ? 0 : 1e4) + if (score < bestScore) { bestScore = score; best = it } + } + return best +} diff --git a/tests/i18n.test.ts b/tests/i18n.test.ts new file mode 100644 index 0000000..2a05504 --- /dev/null +++ b/tests/i18n.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import { en } from '../src/renderer/src/i18n/en' +import { fr } from '../src/renderer/src/i18n/fr' + +describe('traductions', () => { + it('même clés et mêmes variables en FR et EN', () => { + expect(Object.keys(en).sort()).toEqual(Object.keys(fr).sort()) + for (const [k, v] of Object.entries(fr)) { + const vars = (s: string) => (s.match(/\{\w+\}/g) ?? []).sort() + expect(vars(en[k as keyof typeof en]), k).toEqual(vars(v)) + } + }) +}) diff --git a/tests/input.test.ts b/tests/input.test.ts new file mode 100644 index 0000000..345ae1c --- /dev/null +++ b/tests/input.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest' +import { padActions, padKind, Repeater } from '../src/renderer/src/nav/input' +import { glyph } from '../src/renderer/src/nav/glyph-map' + +const pad = (pressed: number[], axes = [0, 0]) => ({ buttons: Array.from({ length: 17 }, (_, i) => ({ pressed: pressed.includes(i), touched: false, value: 0 })), axes }) + +describe('entrées manette', () => { + it('traduit boutons et stick', () => { + expect([...padActions(pad([0, 13]))].sort()).toEqual(['accept', 'down']) + expect([...padActions(pad([], [-0.9, 0.1]))]).toEqual(['left']) + expect(padActions(pad([], [0.3, -0.3])).size).toBe(0) // zone morte + }) + it('reconnaît les manettes PlayStation', () => { + expect(padKind('054c-0ce6-Wireless Controller')).toBe('ps') + expect(padKind('DualSense Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 0ce6)')).toBe('ps') + expect(padKind('Xbox 360 Controller (XInput STANDARD GAMEPAD)')).toBe('xbox') + }) + it('affiche le bon bouton', () => { + expect(glyph('accept', 'xbox')).toBe('A') + expect(glyph('accept', 'ps')).toBe('✕') + expect(glyph('back', 'ps')).toBe('○') + }) +}) + +describe('Repeater', () => { + it('répète une direction tenue, pas un bouton', () => { + const r = new Repeater() + expect(r.step('down', true, 0)).toBe(true) + expect(r.step('down', true, 200)).toBe(false) + expect(r.step('down', true, 400)).toBe(true) + expect(r.step('down', true, 430)).toBe(false) + expect(r.step('down', true, 490)).toBe(true) + expect(r.step('accept', true, 0)).toBe(true) + expect(r.step('accept', true, 1000)).toBe(false) + r.step('down', false, 500) + expect(r.step('down', true, 510)).toBe(true) + }) +}) diff --git a/tests/spatial.test.ts b/tests/spatial.test.ts new file mode 100644 index 0000000..5143ae5 --- /dev/null +++ b/tests/spatial.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest' +import { pickNext, type Box } from '../src/renderer/src/nav/spatial' + +/* grille 3x3 de cartes 100x150, espacées de 20 */ +const grid = Array.from({ length: 9 }, (_, i) => ({ id: i, box: { x: (i % 3) * 120, y: Math.floor(i / 3) * 170, w: 100, h: 150 } as Box })) +const at = (i: number) => grid[i].box +const next = (i: number, d: 'up' | 'down' | 'left' | 'right', items = grid) => pickNext(at(i), items.filter((g) => g.id !== i), d)?.id ?? null + +describe('pickNext', () => { + it('se déplace dans une grille', () => { + expect(next(4, 'right')).toBe(5) + expect(next(4, 'left')).toBe(3) + expect(next(4, 'up')).toBe(1) + expect(next(4, 'down')).toBe(7) + }) + it('ne sort pas de la grille', () => { + expect(next(2, 'right')).toBeNull() + expect(next(0, 'up')).toBeNull() + }) + it('préfère le même axe à la diagonale plus proche', () => { + const items = [{ id: 'loin', box: { x: 600, y: 0, w: 100, h: 50 } }, { id: 'diag', box: { x: 150, y: 80, w: 100, h: 50 } }] + expect(pickNext({ x: 0, y: 0, w: 100, h: 50 }, items, 'right')?.id).toBe('loin') + }) + it('descend vers un bouton large sous la grille', () => { + const items = [...grid, { id: 99, box: { x: 0, y: 600, w: 400, h: 60 } }] + expect(next(8, 'down', items)).toBe(99) + }) + it('remonte de la barre vers la carte la plus alignée', () => { + const items = [...grid, { id: 99, box: { x: 240, y: 600, w: 100, h: 60 } }] + expect(pickNext(items[9].box, grid, 'up')?.id).toBe(8) + }) +}) From b8b1afc62676303c3222c6d222a955e119162a50 Mon Sep 17 00:00:00 2001 From: yasder5 <102179445+Yasder5@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:06:13 +0200 Subject: [PATCH 2/2] =?UTF-8?q?Clavier=20virtuel,=20barre=20d'aide=20des?= =?UTF-8?q?=20boutons=20et=20sons=20de=20navigation=20du=20th=C3=A8me=20Or?= =?UTF-8?q?be=20(g=C3=A9n=C3=A9r=C3=A9s,=20tools/gen-sounds.mjs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/App.tsx | 28 +++++--- src/renderer/src/components/Keyboard.tsx | 83 +++++++++++++++++++++++ src/renderer/src/styles/base.css | 30 ++++++++ src/renderer/src/theme/theme.tsx | 8 ++- themes/orbe/sounds/back.wav | Bin 0 -> 11952 bytes themes/orbe/sounds/error.wav | Bin 0 -> 16802 bytes themes/orbe/sounds/launch.wav | Bin 0 -> 49436 bytes themes/orbe/sounds/move.wav | Bin 0 -> 3132 bytes themes/orbe/sounds/select.wav | Bin 0 -> 11952 bytes themes/orbe/theme.json | 49 ++++++++++--- tools/gen-sounds.mjs | 49 +++++++++++++ 11 files changed, 228 insertions(+), 19 deletions(-) create mode 100644 src/renderer/src/components/Keyboard.tsx create mode 100644 themes/orbe/sounds/back.wav create mode 100644 themes/orbe/sounds/error.wav create mode 100644 themes/orbe/sounds/launch.wav create mode 100644 themes/orbe/sounds/move.wav create mode 100644 themes/orbe/sounds/select.wav create mode 100644 tools/gen-sounds.mjs diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 266544f..af58810 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -1,21 +1,27 @@ +import { useState } from 'react' import { Background } from './components/Background' -import { SettingsProvider, useSettings } from './state/settings' -import { ThemeProvider, useTheme } from './theme/theme' +import { Keyboard } from './components/Keyboard' +import { HintBar } from './nav/glyphs' +import { NavProvider } from './nav/nav' +import { SettingsProvider } from './state/settings' +import { ThemeProvider } from './theme/theme' import './styles/base.css' function Preview() { - const { themes, theme } = useTheme() - const { update } = useSettings() + const [kb, setKb] = useState(false) + const [text, setText] = useState('') return (
-

Thèmes

+

Navigation

Orbe Launcher

-
- {themes.map((t) => ( - - ))} +

{text || '…'}

+
+ {Array.from({ length: 12 }, (_, i) =>
+ + {kb && { setText(v); setKb(false) }} onCancel={() => setKb(false)} />} + {!kb && }
) } @@ -24,7 +30,9 @@ export function App() { return ( - + + + ) diff --git a/src/renderer/src/components/Keyboard.tsx b/src/renderer/src/components/Keyboard.tsx new file mode 100644 index 0000000..1c681b0 --- /dev/null +++ b/src/renderer/src/components/Keyboard.tsx @@ -0,0 +1,83 @@ +/* Clavier virtuel plein écran pour la manette. Le clavier physique tape aussi dedans. */ +import { useEffect, useRef, useState } from 'react' +import { HintBar } from '../nav/glyphs' +import { useNav } from '../nav/nav' +import { useSettings } from '../state/settings' +import { useT } from '../i18n/i18n' + +const LAYOUTS = { + fr: ['1234567890', 'azertyuiop', 'qsdfghjklm', "wxcvbn,.'-"], + en: ['1234567890', 'qwertyuiop', 'asdfghjkl-', "zxcvbnm,.'"] +} + +interface Props { + title?: string + value: string + max?: number + onChange?: (v: string) => void // à chaque frappe (recherche instantanée) + onSubmit: (v: string) => void + onCancel: () => void +} + +export function Keyboard({ title, value: initial, max = 120, onChange, onSubmit, onCancel }: Props) { + const t = useT() + const { settings } = useSettings() + const [value, setValue] = useState(initial) + const [shift, setShift] = useState(false) + const first = useRef(true) + const type = (c: string) => setValue((v) => (v + c).slice(0, max)) + const typeKey = (c: string) => type(shift ? c.toUpperCase() : c) + const del = () => setValue((v) => v.slice(0, -1)) + useEffect(() => { if (first.current) first.current = false; else onChange?.(value) }, [value]) + + /* Clavier physique : caractères et retour arrière vont dans le champ, avant la navigation */ + useEffect(() => { + const k = (e: KeyboardEvent) => { + if (e.ctrlKey || e.altKey || e.metaKey) return + if (e.key.length === 1) type(e.key) + else if (e.key === 'Backspace') del() + else if (e.key === 'Enter' && !(document.activeElement instanceof HTMLButtonElement)) onSubmit(value) + else return + e.preventDefault() + e.stopImmediatePropagation() + } + addEventListener('keydown', k, true) + return () => removeEventListener('keydown', k, true) + }) + + useNav((a) => { + if (a === 'back') { onCancel(); return true } + if (a === 'x') { del(); return true } + if (a === 'y') { type(' '); return true } + if (a === 'lb' || a === 'lt') { setShift((s) => !s); return true } + if (a === 'menu' || a === 'rt') { onSubmit(value); return true } + return false + }) + + return ( +
+
+

{title ?? t('kb.title')}

+
{value}
+
+ {LAYOUTS[settings.language].map((row, i) => ( +
+ {[...row].map((c, j) => ( + + ))} +
+ ))} +
+ + + + +
+
+
+ +
+ ) +} diff --git a/src/renderer/src/styles/base.css b/src/renderer/src/styles/base.css index 266ddb9..022b712 100644 --- a/src/renderer/src/styles/base.css +++ b/src/renderer/src/styles/base.css @@ -47,3 +47,33 @@ mark { background: none; color: var(--accent); } /* Fond animé */ .bg-layer { position: fixed; inset: 0; z-index: -1; pointer-events: none; } .bg-layer canvas, .bg-layer video, .bg-layer img { width: 100%; height: 100%; object-fit: cover; } + +/* Barre d'aide (boutons) */ +.hintbar { position: fixed; inset: auto 0 0 0; height: 64px; display: flex; justify-content: flex-end; align-items: center; gap: 28px; padding: 0 var(--pad); + font: 400 14px/1 var(--font-mono); color: var(--muted); letter-spacing: .06em; z-index: 30; background: linear-gradient(transparent, color-mix(in srgb, var(--bg) 85%, transparent) 40%); } +.hint { display: inline-flex; align-items: center; gap: 10px; } +.glyph { display: inline-grid; place-items: center; min-width: 30px; height: 30px; padding: 0 8px; border-radius: 999px; border: 1px solid var(--line); background: var(--surface-2); color: var(--text); font: 700 13px/1 var(--font-mono); } +.glyph--xbox.glyph--accept { background: #107c10; border-color: #107c10; color: #fff; } +.glyph--xbox.glyph--back { background: #c4302b; border-color: #c4302b; color: #fff; } +.glyph--xbox.glyph--x { background: #1f6fd1; border-color: #1f6fd1; color: #fff; } +.glyph--xbox.glyph--y { background: #d6a400; border-color: #d6a400; color: #111; } +.glyph--keyboard, .glyph--mouse { border-radius: var(--radius-s); } + +/* Fenêtres modales */ +.modal { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; background: color-mix(in srgb, var(--bg) 82%, transparent); backdrop-filter: blur(10px); animation: modal-in var(--dur-med) var(--ease); } +@keyframes modal-in { from { opacity: 0; } } + +/* Clavier virtuel */ +.osk__box { width: min(1100px, 92vw); padding-bottom: 60px; } +.osk__field { min-height: 76px; padding: 18px 22px; margin-bottom: 26px; border-radius: var(--radius-m); border: 1px solid var(--accent); background: var(--surface); font-size: 30px; white-space: pre; overflow: hidden; } +.osk__caret { display: inline-block; width: 3px; height: 1.1em; margin-left: 2px; vertical-align: -.15em; background: var(--accent); } +.osk__keys { display: grid; gap: 10px; } +.osk__row { display: flex; gap: 10px; justify-content: center; } +.osk__key { flex: 1; max-width: 96px; height: 68px; border-radius: var(--radius-s); background: var(--surface); border: 1px solid var(--line); font: 600 24px/1 var(--font-sans); + transition: transform var(--dur-fast) var(--ease), background var(--dur-fast), box-shadow var(--dur-fast); } +.osk__key--wide { max-width: 150px; } +.osk__key--space { max-width: 420px; flex: 4; font-size: 16px; } +.osk__key--ok { max-width: 180px; background: var(--accent); color: var(--on-accent); font-size: 16px; } +.osk__key.is-on { color: var(--accent); } +.osk__key.is-focused { box-shadow: var(--focus-ring); transform: scale(1.06); background: var(--surface-2); } +.osk__key--ok.is-focused { background: var(--accent); } diff --git a/src/renderer/src/theme/theme.tsx b/src/renderer/src/theme/theme.tsx index 28d5ad5..2a4b33c 100644 --- a/src/renderer/src/theme/theme.tsx +++ b/src/renderer/src/theme/theme.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react' import type { ThemeEntry } from '@shared/ipc' import { api } from '../api' +import { loadSounds, setVolume } from '../nav/sounds' import { useSettings } from '../state/settings' interface Ctx { @@ -46,7 +47,12 @@ export function ThemeProvider({ children }: { children: ReactNode }) { return api.on('themes:changed', ({ themes }) => { setThemes(themes); setVersion(Date.now()) }) }, []) const theme = themes.find((t) => t.manifest.id === settings.theme) ?? themes.find((t) => t.manifest.id === 'orbe') ?? themes[0] ?? null - useEffect(() => { if (theme && ready) apply(theme, version) }, [theme, version, ready]) + useEffect(() => { + if (!theme || !ready) return + apply(theme, version) + loadSounds(theme.base, theme.manifest.sounds) + }, [theme, version, ready]) + useEffect(() => setVolume(settings.volume), [settings.volume]) const url = (rel?: string) => (rel && theme ? theme.base + rel : undefined) return {children} } diff --git a/themes/orbe/sounds/back.wav b/themes/orbe/sounds/back.wav new file mode 100644 index 0000000000000000000000000000000000000000..aec33b32267b2a3298d0d1faa50fcd66d5295447 GIT binary patch literal 11952 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 Scgo8<<@MjqfBkR#PWNAafxt%q literal 0 HcmV?d00001 diff --git a/themes/orbe/sounds/launch.wav b/themes/orbe/sounds/launch.wav new file mode 100644 index 0000000000000000000000000000000000000000..c1b95fee2b284b754ca3bd2c06a66a1afae60a78 GIT binary patch literal 49436 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 uj{ePYF=zc9OlTqgK=>5^ literal 0 HcmV?d00001 diff --git a/themes/orbe/sounds/select.wav b/themes/orbe/sounds/select.wav new file mode 100644 index 0000000000000000000000000000000000000000..7774f47244ab277a9423659deddc8dd527465b76 GIT binary patch literal 11952 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 X-`~Ig?tfnYZT)|L{`dR;7oYzNpt$Lx literal 0 HcmV?d00001 diff --git a/themes/orbe/theme.json b/themes/orbe/theme.json index e327f86..bf3de45 100644 --- a/themes/orbe/theme.json +++ b/themes/orbe/theme.json @@ -28,14 +28,47 @@ "--ease": "cubic-bezier(.2, .8, .2, 1)" }, "fonts": [ - { "family": "Archivo", "src": "fonts/Archivo-100-900-latin.woff2", "weight": "100 900", "unicodeRange": "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD" }, - { "family": "Archivo", "src": "fonts/Archivo-100-900-latin-ext.woff2", "weight": "100 900", "unicodeRange": "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+20A0-20AB, U+20AD-20C4, U+2C60-2C7F, U+A720-A7FF" }, - { "family": "JetBrains Mono", "src": "fonts/JetBrainsMono-400-latin.woff2", "weight": "400 700", "unicodeRange": "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD" }, - { "family": "JetBrains Mono", "src": "fonts/JetBrainsMono-400-latin-ext.woff2", "weight": "400 700", "unicodeRange": "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+20A0-20AB, U+20AD-20C4, U+2C60-2C7F, U+A720-A7FF" } + { + "family": "Archivo", + "src": "fonts/Archivo-100-900-latin.woff2", + "weight": "100 900", + "unicodeRange": "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD" + }, + { + "family": "Archivo", + "src": "fonts/Archivo-100-900-latin-ext.woff2", + "weight": "100 900", + "unicodeRange": "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+20A0-20AB, U+20AD-20C4, U+2C60-2C7F, U+A720-A7FF" + }, + { + "family": "JetBrains Mono", + "src": "fonts/JetBrainsMono-400-latin.woff2", + "weight": "400 700", + "unicodeRange": "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD" + }, + { + "family": "JetBrains Mono", + "src": "fonts/JetBrainsMono-400-latin-ext.woff2", + "weight": "400 700", + "unicodeRange": "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+1E00-1E9F, U+20A0-20AB, U+20AD-20C4, U+2C60-2C7F, U+A720-A7FF" + } ], "css": "theme.css", - "layout": { "library": "grid" }, - "background": { "type": "orb" }, - "splash": { "type": "orb", "duration": 1800 }, - "sounds": {} + "layout": { + "library": "grid" + }, + "background": { + "type": "orb" + }, + "splash": { + "type": "orb", + "duration": 1800 + }, + "sounds": { + "move": "sounds/move.wav", + "select": "sounds/select.wav", + "back": "sounds/back.wav", + "error": "sounds/error.wav", + "launch": "sounds/launch.wav" + } } diff --git a/tools/gen-sounds.mjs b/tools/gen-sounds.mjs new file mode 100644 index 0000000..516a234 --- /dev/null +++ b/tools/gen-sounds.mjs @@ -0,0 +1,49 @@ +// Génère les sons de navigation du thème Orbe (WAV 16 bits mono, 44,1 kHz) : sobres, courts, sans dépendance. +// Usage : node tools/gen-sounds.mjs +import { mkdirSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +const RATE = 44100 +const out = join(import.meta.dirname, '..', 'themes', 'orbe', 'sounds') + +/* voix : fréquence qui glisse de f0 à f1, enveloppe attaque/déclin, timbre sinus + un peu d'harmonique */ +function tone(dur, f0, f1, { gain = 0.5, attack = 0.004, harm = 0.15, delay = 0 } = {}) { + const n = Math.round((dur + delay) * RATE), s = new Float32Array(n) + let phase = 0 + for (let i = Math.round(delay * RATE); i < n; i++) { + const t = i / RATE - delay, p = t / dur + const f = f0 * Math.pow(f1 / f0, p) + phase += (2 * Math.PI * f) / RATE + const env = Math.min(1, t / attack) * Math.pow(1 - p, 2.2) + s[i] = gain * env * (Math.sin(phase) + harm * Math.sin(2 * phase)) + } + return s +} + +const mix = (...parts) => { + const n = Math.max(...parts.map((p) => p.length)), s = new Float32Array(n) + for (const p of parts) for (let i = 0; i < p.length; i++) s[i] += p[i] + return s +} + +function wav(samples) { + const b = Buffer.alloc(44 + samples.length * 2) + b.write('RIFF', 0); b.writeUInt32LE(36 + samples.length * 2, 4); b.write('WAVE', 8) + b.write('fmt ', 12); b.writeUInt32LE(16, 16); b.writeUInt16LE(1, 20); b.writeUInt16LE(1, 22) + b.writeUInt32LE(RATE, 24); b.writeUInt32LE(RATE * 2, 28); b.writeUInt16LE(2, 32); b.writeUInt16LE(16, 34) + b.write('data', 36); b.writeUInt32LE(samples.length * 2, 40) + samples.forEach((v, i) => b.writeInt16LE(Math.round(Math.max(-1, Math.min(1, v)) * 32767), 44 + i * 2)) + return b +} + +const sounds = { + move: tone(0.035, 2400, 2200, { gain: 0.18, harm: 0 }), // petit tic + select: mix(tone(0.07, 660, 680, { gain: 0.32 }), tone(0.09, 990, 1000, { gain: 0.28, delay: 0.045 })), // deux notes qui montent + back: mix(tone(0.07, 740, 720, { gain: 0.28 }), tone(0.09, 494, 480, { gain: 0.26, delay: 0.045 })), // deux notes qui descendent + error: mix(tone(0.08, 180, 170, { gain: 0.35, harm: 0.4 }), tone(0.08, 180, 170, { gain: 0.35, harm: 0.4, delay: 0.11 })), + launch: mix(tone(0.45, 330, 1320, { gain: 0.3, attack: 0.02 }), tone(0.5, 495, 1980, { gain: 0.12, attack: 0.05, delay: 0.06 })) +} + +mkdirSync(out, { recursive: true }) +for (const [name, s] of Object.entries(sounds)) writeFileSync(join(out, name + '.wav'), wav(s)) +console.log('sons écrits dans', out)