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() {
+
+
+
+
+