From efb09efa8489dc11afa78ea35fc45abacf9bb137 Mon Sep 17 00:00:00 2001 From: Ali Shair Date: Tue, 10 Feb 2026 22:33:01 +0500 Subject: [PATCH 1/4] feat: server-config.json for server settings only and document client vs server --- src/config.tsx | 35 ++++++++++++++++++++--------------- src/routes/settings.tsx | 5 +++-- src/server/websocket.ts | 12 +++++++++++- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/config.tsx b/src/config.tsx index dac1942..9a89245 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -1,22 +1,27 @@ -import serverConfig from './server-config.json'; +import serverConfig from "./server-config.json"; +/** + * Server vs client settings: + * - Server settings (server-config.json): host, frontendPort, address, mouseInvert, mouseSensitivity. + * Updated only when user clicks Save Config. + * - Client settings (localStorage only): e.g. rein_ip, rein-theme. Never in server-config.json. + */ 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", +}; +/** Server-only config (from server-config.json). Do not add client-only keys here. */ 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, + MOUSE_INVERT: serverConfig.mouseInvert ?? false, + MOUSE_SENSITIVITY: serverConfig.mouseSensitivity ?? 1.0, }; diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index dde409f..c91e5e0 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -14,7 +14,7 @@ function SettingsPage() { const [sensitivity, setSensitivity] = useState(CONFIG.MOUSE_SENSITIVITY); const [qrData, setQrData] = useState(''); - // Load initial state + // Client-only setting: stored in localStorage only, never sent to server-config.json useEffect(() => { const storedIp = localStorage.getItem('rein_ip'); const defaultIp = typeof window !== 'undefined' ? window.location.hostname : 'localhost'; @@ -177,10 +177,11 @@ function SettingsPage() { const socket = new WebSocket(wsUrl); socket.onopen = () => { + // Only server settings; client settings (like ip) stay in localStorage socket.send(JSON.stringify({ type: 'update-config', config: { - frontendPort: parseInt(frontendPort), + frontendPort: parseInt(frontendPort, 10), mouseInvert: invertScroll, mouseSensitivity: sensitivity, } diff --git a/src/server/websocket.ts b/src/server/websocket.ts index f06ad7a..d48c3b5 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -54,10 +54,20 @@ export function createWsServer(server: Server) { if (msg.type === 'update-config') { console.log('Updating config:', msg.config); try { + // Only server settings belong in server-config.json. Client settings (like IP, theme) + // must be stored on the client (localStorage) only and must never be written here. + const SERVER_CONFIG_KEYS = ['host', 'frontendPort', 'address', 'mouseInvert', 'mouseSensitivity'] as const; const configPath = './src/server-config.json'; // eslint-disable-next-line @typescript-eslint/no-require-imports const current = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {}; - const newConfig = { ...current, ...msg.config }; + const incoming = msg.config || {}; + const filtered: Record = {}; + for (const key of SERVER_CONFIG_KEYS) { + if (Object.prototype.hasOwnProperty.call(incoming, key)) { + filtered[key] = incoming[key]; + } + } + const newConfig = { ...current, ...filtered }; fs.writeFileSync(configPath, JSON.stringify(newConfig, null, 2)); ws.send(JSON.stringify({ type: 'config-updated', success: true })); From a67b0786230ffa973467c57548942d504542057a Mon Sep 17 00:00:00 2001 From: Ali Shair Date: Wed, 11 Feb 2026 17:27:52 +0500 Subject: [PATCH 2/4] make changes accordingly as suggested by sir Rituraj as ('host', 'frontendPort', 'address') only are for server --- src/config.tsx | 9 ++++----- src/server/websocket.ts | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/config.tsx b/src/config.tsx index 9a89245..4686233 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -2,9 +2,8 @@ import serverConfig from "./server-config.json"; /** * Server vs client settings: - * - Server settings (server-config.json): host, frontendPort, address, mouseInvert, mouseSensitivity. - * Updated only when user clicks Save Config. - * - Client settings (localStorage only): e.g. rein_ip, rein-theme. Never in server-config.json. + * - Server config (server-config.json): host, frontendPort, address are the only keys writable via Save Config. + * mouseInvert/mouseSensitivity may exist in the file for reading but are not updated from the client. */ export const APP_CONFIG = { SITE_NAME: "Rein", @@ -19,9 +18,9 @@ export const THEMES = { DEFAULT: "dracula", }; -/** Server-only config (from server-config.json). Do not add client-only keys here. */ +/** Config from server-config.json. Only host, frontendPort, address are writable via Settings Save. */ export const CONFIG = { - FRONTEND_PORT: serverConfig.frontendPort, + FRONTEND_PORT: serverConfig.frontendPort ?? 3000, MOUSE_INVERT: serverConfig.mouseInvert ?? false, MOUSE_SENSITIVITY: serverConfig.mouseSensitivity ?? 1.0, }; diff --git a/src/server/websocket.ts b/src/server/websocket.ts index d48c3b5..92edd3b 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -54,9 +54,8 @@ export function createWsServer(server: Server) { if (msg.type === 'update-config') { console.log('Updating config:', msg.config); try { - // Only server settings belong in server-config.json. Client settings (like IP, theme) - // must be stored on the client (localStorage) only and must never be written here. - const SERVER_CONFIG_KEYS = ['host', 'frontendPort', 'address', 'mouseInvert', 'mouseSensitivity'] as const; + // Only server config: host, port, address. Client settings (IP, theme, sensitivity, invert) are not written here. + const SERVER_CONFIG_KEYS = ['host', 'frontendPort', 'address'] as const; const configPath = './src/server-config.json'; // eslint-disable-next-line @typescript-eslint/no-require-imports const current = fs.existsSync(configPath) ? JSON.parse(fs.readFileSync(configPath, 'utf-8')) : {}; From 958736ef56139b463c6f2db2fa1a784946a9c511 Mon Sep 17 00:00:00 2001 From: Ali Shair Date: Wed, 11 Feb 2026 19:56:39 +0500 Subject: [PATCH 3/4] Client settings (Mouse sensitivity, invert scroll, theme) in localStorage and reactive, --- src/config.tsx | 5 +- src/hooks/useTrackpadGesture.ts | 22 +++-- src/routes/__root.tsx | 12 +++ src/routes/settings.tsx | 158 +++++++++++++++++++++++--------- src/server/websocket.ts | 11 ++- 5 files changed, 152 insertions(+), 56 deletions(-) diff --git a/src/config.tsx b/src/config.tsx index 4686233..a70be0f 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -1,9 +1,8 @@ import serverConfig from "./server-config.json"; /** - * Server vs client settings: - * - Server config (server-config.json): host, frontendPort, address are the only keys writable via Save Config. - * mouseInvert/mouseSensitivity may exist in the file for reading but are not updated from the client. + * 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", diff --git a/src/hooks/useTrackpadGesture.ts b/src/hooks/useTrackpadGesture.ts index 0072896..2ab97a6 100644 --- a/src/hooks/useTrackpadGesture.ts +++ b/src/hooks/useTrackpadGesture.ts @@ -16,10 +16,20 @@ 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 }; +} + export const useTrackpadGesture = ( send: (msg: any) => void, scrollMode: boolean, - sensitivity: number = 1.5 + _defaultSensitivity: number = 1.5 ) => { const [isTracking, setIsTracking] = useState(false); @@ -129,8 +139,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; @@ -140,13 +152,11 @@ 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 - send({ type: 'scroll', dx: -sumX * sensitivity, dy: -sumY * sensitivity }); + send({ type: 'scroll', dx: sumX * sensitivity * scrollSign, dy: sumY * sensitivity * scrollSign }); } else if (ongoingTouches.current.length === 1 || dragging.current) { - // Cursor movement (only in cursor mode with 1 finger, or when dragging) send({ type: 'move', dx: sumX * sensitivity, dy: sumY * sensitivity }); } } diff --git a/src/routes/__root.tsx b/src/routes/__root.tsx index 686b73c..1401f17 100644 --- a/src/routes/__root.tsx +++ b/src/routes/__root.tsx @@ -1,7 +1,9 @@ import { Outlet, createRootRoute, Link, Scripts, HeadContent } from '@tanstack/react-router' // import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' import * as React from 'react' +import { useEffect } from 'react' import '../styles.css' +import { APP_CONFIG, THEMES } from '../config' export const Route = createRootRoute({ component: RootComponent, @@ -24,6 +26,15 @@ function RootComponent() { ) } +function ThemeInit() { + useEffect(() => { + const saved = typeof localStorage !== 'undefined' && localStorage.getItem(APP_CONFIG.THEME_STORAGE_KEY) + const theme = saved === THEMES.LIGHT || saved === THEMES.DARK ? saved : THEMES.DEFAULT + document.documentElement.setAttribute('data-theme', theme) + }, []) + return null +} + function RootDocument({ children }: { children: React.ReactNode }) { return ( @@ -35,6 +46,7 @@ function RootDocument({ children }: { children: React.ReactNode }) { +
diff --git a/src/routes/settings.tsx b/src/routes/settings.tsx index c91e5e0..74235dd 100644 --- a/src/routes/settings.tsx +++ b/src/routes/settings.tsx @@ -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) - // Client-only setting: stored in localStorage only, never sent to server-config.json + // 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() {
+
+ + +
+