diff --git a/src/config.tsx b/src/config.tsx index dac1942..a70be0f 100644 --- a/src/config.tsx +++ b/src/config.tsx @@ -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, }; diff --git a/src/hooks/useTrackpadGesture.ts b/src/hooks/useTrackpadGesture.ts index 1c56a0a..41bdce1 100644 --- a/src/hooks/useTrackpadGesture.ts +++ b/src/hooks/useTrackpadGesture.ts @@ -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 }; +} + 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, + }); } 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, + }); } } }; 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 dde409f..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) - // 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() {
+
+ + +
+