-
Notifications
You must be signed in to change notification settings - Fork 27
Save client-side settings on the client; server-config.json for server only #59
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
efb09ef
a67b078
958736e
c4d173e
aef3aad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,25 @@ | ||
| import serverConfig from './server-config.json'; | ||
| import serverConfig from "./server-config.json"; | ||
|
|
||
| /** | ||
| * Server config (server-config.json): only host, frontendPort, address are writable via Save Config. | ||
| * Client settings (mouse sensitivity, invert scroll, theme) are stored in localStorage only. | ||
| */ | ||
| export const APP_CONFIG = { | ||
| SITE_NAME: "Rein", | ||
| SITE_DESCRIPTION: "Remote controller for your PC", | ||
| REPO_URL: "https://github.com/imxade/rein", | ||
| THEME_STORAGE_KEY: "rein-theme", | ||
| } | ||
| SITE_NAME: "Rein", | ||
| SITE_DESCRIPTION: "Remote controller for your PC", | ||
| REPO_URL: "https://github.com/imxade/rein", | ||
| THEME_STORAGE_KEY: "rein-theme", | ||
| }; | ||
|
|
||
| export const THEMES = { | ||
| LIGHT: 'cupcake', | ||
| DARK: 'dracula', | ||
| DEFAULT: 'dracula', | ||
| } | ||
| LIGHT: "cupcake", | ||
| DARK: "dracula", | ||
| DEFAULT: "dracula", | ||
| }; | ||
|
|
||
| /** Config from server-config.json. Only host, frontendPort, address are writable via Settings Save. */ | ||
| export const CONFIG = { | ||
| // Port for the Vite Frontend | ||
| FRONTEND_PORT: serverConfig.frontendPort, | ||
| MOUSE_INVERT: serverConfig.mouseInvert ?? false, | ||
| // Default to 1.0 if not set | ||
| MOUSE_SENSITIVITY: serverConfig.mouseSensitivity ?? 1.0 | ||
| FRONTEND_PORT: serverConfig.frontendPort ?? 3000, | ||
| MOUSE_INVERT: serverConfig.mouseInvert ?? false, | ||
| MOUSE_SENSITIVITY: serverConfig.mouseSensitivity ?? 1.0, | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -16,11 +16,21 @@ const getTouchDistance = (a: TrackedTouch, b: TrackedTouch): number => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Math.sqrt(dx * dx + dy * dy); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CLIENT_KEYS = { SENSITIVITY: 'rein_mouse_sensitivity', INVERT: 'rein_mouse_invert' } as const; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function getClientSettings(): { sensitivity: number; invert: boolean } { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof localStorage === 'undefined') return { sensitivity: 1.0, invert: false }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const s = localStorage.getItem(CLIENT_KEYS.SENSITIVITY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : 1.0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const invert = localStorage.getItem(CLIENT_KEYS.INVERT) === 'true'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { sensitivity, invert }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+21
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
-function getClientSettings(): { sensitivity: number; invert: boolean } {
- if (typeof localStorage === 'undefined') return { sensitivity: 1.0, invert: false };
+function getClientSettings(defaultSensitivity: number = 1.0): { sensitivity: number; invert: boolean } {
+ if (typeof localStorage === 'undefined') return { sensitivity: defaultSensitivity, invert: false };
const s = localStorage.getItem(CLIENT_KEYS.SENSITIVITY);
- const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : 1.0;
+ const sensitivity = s !== null && !Number.isNaN(Number(s)) ? Number(s) : defaultSensitivity;…and call it as Also applies to: 32-33 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const useTrackpadGesture = ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send: (msg: any) => void, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollMode: boolean, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sensitivity: number = 1.5, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| axisThreshold: number = 2.5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _defaultSensitivity: number = 1.5, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| axisThreshold: number = 2.5 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [isTracking, setIsTracking] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -130,8 +140,10 @@ export const useTrackpadGesture = ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| tracked.timeStamp = e.timeStamp; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Send movement if we've moved and not in timeout period | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Send movement if we've moved and not in timeout period; use client settings so changes take effect immediately | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (moved.current && e.timeStamp - lastEndTimeStamp.current >= TOUCH_TIMEOUT) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { sensitivity, invert } = getClientSettings(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollSign = invert ? 1 : -1; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!scrollMode && ongoingTouches.current.length === 2) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dist = getTouchDistance(ongoingTouches.current[0], ongoingTouches.current[1]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const delta = lastPinchDist.current !== null ? dist - lastPinchDist.current : 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -141,27 +153,30 @@ export const useTrackpadGesture = ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send({ type: 'zoom', delta: delta * sensitivity }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastPinchDist.current = dist; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send({ type: 'scroll', dx: -sumX * sensitivity, dy: -sumY * sensitivity }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send({ type: 'scroll', dx: sumX * sensitivity * scrollSign, dy: sumY * sensitivity * scrollSign }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (scrollMode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scroll mode: single finger scrolls, or two-finger scroll in cursor mode | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let scrollDx = sumX; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let scrollDy = sumY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Scroll mode: single finger scrolls; dominant axis (from main) for cleaner scroll | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollDx = -sumX; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const scrollDy = -sumY; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const absDx = Math.abs(scrollDx); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const absDy = Math.abs(scrollDy); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (scrollMode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (absDx > absDy * axisThreshold) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Horizontal is dominant - ignore vertical | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollDy = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (absDy > absDx * axisThreshold) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Vertical is dominant - ignore horizontal | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scrollDx = 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send({ type: 'scroll', dx: Math.round(-scrollDx * sensitivity * 10) / 10 , dy: Math.round(-scrollDy * sensitivity * 10) / 10 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const useHorizontal = absDx > axisThreshold * absDy; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const useVertical = absDy > axisThreshold * absDx; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dx = useHorizontal && !useVertical ? scrollDx : 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const dy = useVertical && !useHorizontal ? scrollDy : 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'scroll', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dx: Math.round(dx * sensitivity * scrollSign * 10) / 10, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dy: Math.round(dy * sensitivity * scrollSign * 10) / 10, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
158
to
172
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Lines 160–161 negate Remove the upfront negation so both scroll paths share the same direction convention: 🐛 Proposed fix } else if (scrollMode) {
- // Scroll mode: single finger scrolls; dominant axis (from main) for cleaner scroll
- const scrollDx = -sumX;
- const scrollDy = -sumY;
+ // Scroll mode: single finger scrolls; dominant axis for cleaner scroll
+ const scrollDx = sumX;
+ const scrollDy = sumY;
const absDx = Math.abs(scrollDx);
const absDy = Math.abs(scrollDy);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (ongoingTouches.current.length === 1 || dragging.current) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Cursor movement (only in cursor mode with 1 finger, or when dragging) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send({ type: 'move', dx: Math.round(sumX * sensitivity * 10) / 10 , dy: Math.round(sumY * sensitivity * 10) / 10 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| send({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'move', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dx: Math.round(sumX * sensitivity * 10) / 10, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| dy: Math.round(sumY * sensitivity * 10) / 10, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,45 +1,91 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { createFileRoute } from '@tanstack/react-router' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useEffect } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useEffect, useRef } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import QRCode from 'qrcode'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CONFIG } from '../config'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { CONFIG, APP_CONFIG, THEMES } from '../config'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export const Route = createFileRoute('/settings')({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| component: SettingsPage, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Client-only settings: stored in localStorage, never sent to server-config.json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const CLIENT_KEYS = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| SENSITIVITY: 'rein_mouse_sensitivity', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| INVERT: 'rein_mouse_invert', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| THEME: APP_CONFIG.THEME_STORAGE_KEY, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } as const | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_SENSITIVITY = 1.0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const DEFAULT_INVERT = false | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function SettingsPage() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [ip, setIp] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [frontendPort, setFrontendPort] = useState(String(CONFIG.FRONTEND_PORT)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [invertScroll, setInvertScroll] = useState(CONFIG.MOUSE_INVERT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [sensitivity, setSensitivity] = useState(CONFIG.MOUSE_SENSITIVITY); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [qrData, setQrData] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [ip, setIp] = useState('') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [frontendPort, setFrontendPort] = useState(String(CONFIG.FRONTEND_PORT)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [invertScroll, setInvertScroll] = useState(DEFAULT_INVERT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [sensitivity, setSensitivity] = useState(DEFAULT_SENSITIVITY) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [theme, setTheme] = useState(THEMES.DEFAULT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [qrData, setQrData] = useState('') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const hasLoadedFromStorage = useRef(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isFirstSensitivity = useRef(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isFirstInvert = useRef(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isFirstTheme = useRef(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Load initial state | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Load client settings from localStorage only on client, so they persist when navigating back | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const storedIp = localStorage.getItem('rein_ip'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const defaultIp = typeof window !== 'undefined' ? window.location.hostname : 'localhost'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined') return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const storedIp = localStorage.getItem('rein_ip') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const defaultIp = window.location.hostname || 'localhost' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIp(storedIp || defaultIp) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFrontendPort(String(CONFIG.FRONTEND_PORT)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const s = localStorage.getItem(CLIENT_KEYS.SENSITIVITY) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (s !== null) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const n = Number(s) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!Number.isNaN(n)) setSensitivity(n) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const inv = localStorage.getItem(CLIENT_KEYS.INVERT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (inv === 'true') setInvertScroll(true) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (inv === 'false') setInvertScroll(false) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setIp(storedIp || defaultIp); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // We don't store frontend port in local storage for now, just load from config default | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setFrontendPort(String(CONFIG.FRONTEND_PORT)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const t = localStorage.getItem(CLIENT_KEYS.THEME) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (t === THEMES.LIGHT || t === THEMES.DARK) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTheme(t) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.documentElement.setAttribute('data-theme', t) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| hasLoadedFromStorage.current = true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Effect: Update LocalStorage and Generate QR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Persist client settings to localStorage after user changes (skip first run to avoid overwriting loaded values) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined' || !hasLoadedFromStorage.current) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isFirstSensitivity.current) { isFirstSensitivity.current = false; return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem(CLIENT_KEYS.SENSITIVITY, String(sensitivity)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [sensitivity]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined' || !hasLoadedFromStorage.current) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isFirstInvert.current) { isFirstInvert.current = false; return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem(CLIENT_KEYS.INVERT, String(invertScroll)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [invertScroll]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ip) return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem('rein_ip', ip); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window === 'undefined' || !hasLoadedFromStorage.current) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isFirstTheme.current) { isFirstTheme.current = false; return } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem(CLIENT_KEYS.THEME, theme) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| document.documentElement.setAttribute('data-theme', theme) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [theme]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Update LocalStorage for IP and generate QR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!ip) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| localStorage.setItem('rein_ip', ip) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof window !== 'undefined') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Point to Frontend | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const appPort = String(CONFIG.FRONTEND_PORT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const protocol = window.location.protocol; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const shareUrl = `${protocol}//${ip}:${appPort}/trackpad`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const appPort = String(CONFIG.FRONTEND_PORT) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const protocol = window.location.protocol | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const shareUrl = `${protocol}//${ip}:${appPort}/trackpad` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| QRCode.toDataURL(shareUrl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .then(setQrData) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((e) => console.error('QR Error:', e)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| .catch((e) => console.error('QR Error:', e)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [ip]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [ip]) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Effect: Auto-detect LAN IP from Server (only if on localhost) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -146,6 +192,20 @@ function SettingsPage() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="form-control w-full"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label className="label"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="label-text">Theme</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <select | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="select select-bordered w-full" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={theme} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={(e) => setTheme(e.target.value as typeof THEMES.LIGHT | typeof THEMES.DARK)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <option value={THEMES.DARK}>Dark (dracula)</option> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <option value={THEMES.LIGHT}>Light (cupcake)</option> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </select> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+195
to
+207
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessibility: Static analysis flagged this — the Proposed fix <div className="form-control w-full">
- <label className="label">
+ <label className="label" htmlFor="theme-select">
<span className="label-text">Theme</span>
</label>
<select
+ id="theme-select"
className="select select-bordered w-full"
value={theme}📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.3.14)[error] 196-198: A form label must be associated with an input. Consider adding a (lint/a11y/noLabelWithoutControl) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="form-control w-full"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <label className="label"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <span className="label-text">Port</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -171,30 +231,41 @@ function SettingsPage() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="btn btn-neutral w-full" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const host = window.location.host; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wsUrl = `${protocol}//${host}/ws`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const socket = new WebSocket(wsUrl); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const portNum = parseInt(frontendPort, 10) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert('Please enter a valid port (1–65535).') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const host = window.location.host | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const wsUrl = `${protocol}//${host}/ws` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const socket = new WebSocket(wsUrl) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.onerror = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert('Could not connect to server. Is the app running?') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.onopen = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.send(JSON.stringify({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: 'update-config', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| frontendPort: parseInt(frontendPort), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mouseInvert: invertScroll, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mouseSensitivity: sensitivity, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| config: { frontendPort: portNum }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.onmessage = (event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const data = JSON.parse(event.data) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (data.type === 'config-updated' && data.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.close() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newProtocol = window.location.protocol | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newHostname = window.location.hostname | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newUrl = `${newProtocol}//${newHostname}:${portNum}/settings` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { window.location.href = newUrl }, 800) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else if (data.type === 'config-updated' && !data.success) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert('Failed to save config: ' + (data.error || 'Unknown error')) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Give server time to write config and restart | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.close(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newProtocol = window.location.protocol; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newHostname = window.location.hostname; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const newUrl = `${newProtocol}//${newHostname}:${frontendPort}/settings`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = newUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 1000); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (_e) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ignore non-JSON messages | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
231
to
+268
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Save button has no guard against double-clicks or loading feedback. If the user clicks "Save Config" multiple times, each click opens a new WebSocket and sends a config update. This could cause multiple redirects racing or confusing error messages. Consider disabling the button while the save is in progress and showing a loading indicator. Additionally, the WebSocket created in the Sketch: add a saving guard+const [saving, setSaving] = useState(false)
...
<button
- className="btn btn-neutral w-full"
+ className="btn btn-neutral w-full"
+ disabled={saving}
onClick={() => {
+ if (saving) return
+ setSaving(true)
const portNum = parseInt(frontendPort, 10)
if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
alert('Please enter a valid port (1–65535).')
+ setSaving(false)
return
}
...
+ socket.onerror = () => {
+ alert('Could not connect to server. Is the app running?')
+ setSaving(false)
+ }
}}
>
- Save Config
+ {saving ? 'Saving…' : 'Save Config'}
</button>🧰 Tools🪛 Biome (2.3.14)[error] 231-271: Provide an explicit type prop for the button element. The default type of a button is submit, which causes the submission of a form when placed inside a (lint/a11y/useButtonType) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Save Config | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getClientSettingshelper is clean and SSR-safe — nice addition.The
typeof localStorageguard and theNumber.isNaNvalidation are well done. One minor note:Number('')is0andNumber.isNaN(0)isfalse, so an empty string in localStorage would parse as sensitivity0(effectively disabling movement). If that's undesirable, consider addingNumber(s) > 0ors !== ''to the guard.🤖 Prompt for AI Agents