Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
d7945be
Added changes from personal repo
Feb 4, 2026
544993d
Added changes from personal repo
Feb 4, 2026
cb39890
Added download and unzip function to updater
Feb 4, 2026
da32498
Fix bug with nested folders in unzip function
Feb 4, 2026
6046396
Fix bug with nested folders in unzip function
Feb 5, 2026
66e6066
Untracked some files
Feb 5, 2026
185c82a
Added code for installing steamvr driver on windows
Feb 9, 2026
a1205d3
Update executeShellCommand and SteamVR driver install windows
HannahPadd Feb 9, 2026
971dd2c
Moved updater to its own package, made everything less bad, added kto…
Feb 10, 2026
802b011
Added version checking
Feb 10, 2026
f3d05e9
Update gradle config
Feb 10, 2026
2d78777
Move updater to its own module
Feb 11, 2026
bfea802
Move updater to its own module
Feb 13, 2026
ada87ff
Added installation of udev rules to updater
Feb 16, 2026
5d7c65e
Added logic specifically for the steam frame
Feb 16, 2026
248f1a6
Added swing ui to updater to give user feedback
Feb 17, 2026
f2223e5
Merge remote-tracking branch 'origin/llelievr/electron' into hannah/i…
Feb 17, 2026
be476fe
Electron now attempts to launch the updater when then the gui starts
Feb 17, 2026
398e964
I guess I fixed an error with the linux feeder not unzipping
Feb 18, 2026
a5bee87
Working on updated unzip function. Updated download function
Feb 18, 2026
d58d6f8
Merge remote-tracking branch 'origin/llelievr/electron' into hannah/i…
Feb 18, 2026
5ab3075
Update unzip to support progress display
Feb 18, 2026
4053052
Make downloaded files match checksum of remote files
Feb 18, 2026
0f866c4
Added function to calculate file checksum
Feb 18, 2026
24fe01e
Brain owie
Feb 18, 2026
e4fc57a
Merge branch 'llelievr/electron' into hannah/install-from-server
HannahPadd Feb 19, 2026
f2243d3
update unzip function
HannahPadd Feb 19, 2026
2dd8fd9
update electron to launch updater
HannahPadd Feb 19, 2026
5954f9c
Merge remote-tracking branch 'origin/llelievr/electron' into hannah/i…
Feb 20, 2026
acb3873
Added much needed cuteness
Feb 20, 2026
e2ff92e
Merge remote-tracking branch 'origin/hannah/install-from-server' into…
Feb 20, 2026
e2a665d
Windows also needed some cuteness
Feb 20, 2026
3873eb5
Added logic to make window draggable
Feb 20, 2026
1f97b49
Messing around with multithreading the unzip function
Feb 20, 2026
7bcdd68
Attempt at adding coroutine to unzip to speed it up
Feb 20, 2026
d8900d7
unzip is fast
HannahPadd Feb 20, 2026
6322fac
Remove progress callback from unzipworker
HannahPadd Feb 20, 2026
1372632
Updater pulls checksum from github
Feb 23, 2026
d3be840
Merge remote-tracking branch 'origin/llelievr/electron' into hannah/i…
Feb 23, 2026
d0bf170
released to steam beta testers
Feb 23, 2026
e7dc16e
Update logging
Feb 23, 2026
bcda3ac
Started work on a modal to tell linux users to install udev rules if …
Feb 24, 2026
5f64873
UdevrulesModal now reacts to installed udev rules
Feb 24, 2026
242cdd4
lint
Feb 24, 2026
bd9ea99
Added modal config
Feb 24, 2026
77a203c
Push latest changes
Feb 25, 2026
517e76b
Update updater/desktop/src/main/java/dev/slimevr/updater/UpdaterUtils.kt
HannahPadd Feb 26, 2026
41ef425
Added electronAPI function to get install dir inside the react applic…
HannahPadd Feb 26, 2026
9957abb
Finished getWorkingDir()
HannahPadd Feb 26, 2026
13aabd7
Update udev modal, moved sentry consent to page
HannahPadd Feb 27, 2026
f28b5a3
Should be mostly good now
HannahPadd Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ spotlessVersion=8.0.0
shadowJarVersion=8.3.2
buildconfigVersion=5.5.0
grgitVersion=5.2.2
ktor_version=3.0.3
6 changes: 5 additions & 1 deletion gui/electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
findSystemJRE,
getGuiDataFolder,
getLogsFolder,
getExeFolder,
getServerDataFolder,
getWindowStateFile,
} from './paths';
Expand Down Expand Up @@ -171,6 +172,8 @@ handleIpc(IPC_CHANNELS.GET_FOLDER, (e, folder) => {
return getGuiDataFolder();
case 'logs':
return getLogsFolder();
case 'exe':
return getExeFolder();
}
});

Expand Down Expand Up @@ -340,7 +343,6 @@ const spawnServer = async () => {
return;
}


const serverJar = findServerJar();
if (!serverJar) {
logger.info('server jar not found, skipping');
Expand All @@ -359,6 +361,7 @@ const spawnServer = async () => {

logger.info({ serverJar }, 'found server jar');


const process = spawn(javaBin, ['-Xmx128M', '-jar', serverJar, 'run']);

process.stdout?.on('data', (message) => {
Expand Down Expand Up @@ -392,6 +395,7 @@ app.whenReady().then(async () => {
});

checkEnvironmentVariables();

const server = await spawnServer();

createWindow();
Expand Down
6 changes: 5 additions & 1 deletion gui/electron/main/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ export const getLogsFolder = () => {
return join(getGuiDataFolder(), 'logs');
};

export const getExeFolder = () => {
return path.dirname(app.getPath('exe'));
}

export const getWindowStateFile = () =>
join(getServerDataFolder(), '.window-state.json');
join(getGuiDataFolder(), '.window-state.json');

const localJavaBin = (sharedDir: string) => {
const jre = join(sharedDir, 'jre/bin', javaBin);
Expand Down
3 changes: 2 additions & 1 deletion gui/electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ contextBridge.exposeInMainWorld('electronAPI', {
openLogsFolder: async () => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, await ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'logs')),
openFile: (path) => ipcRenderer.invoke(IPC_CHANNELS.OPEN_FILE, path),
ghGet: (req) => ipcRenderer.invoke(IPC_CHANNELS.GH_FETCH, req),
setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options)
setPresence: (options) => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_PRESENCE, options),
getInstallDir: () => ipcRenderer.invoke(IPC_CHANNELS.GET_FOLDER, 'exe')
} satisfies IElectronAPI);
1 change: 1 addition & 0 deletions gui/electron/preload/interface.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface IElectronAPI {
openFile: (path: string) => void;
ghGet: <T extends GHGet>(options: T) => Promise<GHReturn[T['type']]>;
setPresence: (options: DiscordPresence) => void;
getInstallDir: () => Promise<string>;
}

declare global {
Expand Down
2 changes: 1 addition & 1 deletion gui/electron/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export interface IpcInvokeMap {
value?: unknown;
}) => Promise<unknown>;
[IPC_CHANNELS.OPEN_FILE]: (path: string) => void;
[IPC_CHANNELS.GET_FOLDER]: (folder: 'config' | 'logs') => string;
[IPC_CHANNELS.GET_FOLDER]: (folder: 'config' | 'logs' | 'exe') => string;
[IPC_CHANNELS.GH_FETCH]: <T extends GHGet>(
options: T
) => Promise<GHReturn[T['type']]>;
Expand Down
4 changes: 4 additions & 0 deletions gui/public/i18n/en/translation.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,10 @@ onboarding-reset_tutorial-2 = Tap the highlighted tracker { $taps } times to tri

You need to be in a pose like you are skiing as shown in the Automatic Mounting wizard, and you have a 3 second delay (configurable) before it gets triggered.

## Install info
install-info_udev-rules_modal_title = UDEV Rules not found
install-info_udev-rules_warning = Please make sure your udev rules are setup correctly. So you can connect trackers and dongle to USB
install-info_udev-rules_modal_button = Close
## Setup start
onboarding-home = Welcome to SlimeVR
onboarding-home-start = Let's get set up!
Expand Down
7 changes: 7 additions & 0 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AutomaticProportionsPage } from './components/onboarding/pages/body-pro
import { ManualProportionsPage } from './components/onboarding/pages/body-proportions/ManualProportions';
import { ConnectTrackersPage } from './components/onboarding/pages/ConnectTracker';
import { HomePage } from './components/onboarding/pages/Home';
import { ErrorCollectingConsentPage } from './components/onboarding/pages/ErrorCollectingConstent';
import { AutomaticMountingPage } from './components/onboarding/pages/mounting/AutomaticMounting';
import { ManualMountingPage } from './components/onboarding/pages/mounting/ManualMounting';
import { TrackersAssignPage } from './components/onboarding/pages/trackers-assign/TrackerAssignment';
Expand Down Expand Up @@ -53,6 +54,7 @@ import { ChecklistPage } from './components/tracking-checklist/TrackingChecklist
import { ElectronContextC, provideElectron } from './hooks/electron';
import { AppLocalizationProvider } from './i18n/config';
import { openUrl } from './hooks/crossplatform';
import { UdevRulesModal } from './components/onboarding/UdevRulesModal';

export const GH_REPO = 'SlimeVR/SlimeVR-Server';
export const VersionContext = createContext('');
Expand All @@ -70,6 +72,7 @@ function Layout() {
<SerialDetectionModal />
<VersionUpdateModal />
<UnknownDeviceModal />
<UdevRulesModal />
<SentryRoutes>
<Route element={<AppLayout />}>
<Route
Expand Down Expand Up @@ -147,6 +150,10 @@ function Layout() {
}
>
<Route path="home" element={<HomePage />} />
<Route
path="error-collecting-consent"
element={<ErrorCollectingConsentPage />}
/>
<Route path="wifi-creds" element={<WifiCredsPage />} />
<Route path="connect-trackers" element={<ConnectTrackersPage />} />
<Route path="trackers-assign" element={<TrackersAssignPage />} />
Expand Down
11 changes: 8 additions & 3 deletions gui/src/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { useLayoutEffect } from 'react';
import { useConfig } from './hooks/config';
import { Outlet, useNavigate } from 'react-router-dom';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';

export function AppLayout() {
const { config } = useConfig();
const { pathname } = useLocation();
const navigate = useNavigate();

useLayoutEffect(() => {
Expand All @@ -28,10 +29,14 @@ export function AppLayout() {
}, [config]);

useLayoutEffect(() => {
if (config && !config.doneOnboarding) {
if (
config &&
!config.doneOnboarding &&
!pathname.startsWith('/onboarding/')
) {
navigate('/onboarding/home');
}
}, [config?.doneOnboarding]);
}, [config]);

return (
<>
Expand Down
6 changes: 0 additions & 6 deletions gui/src/components/TopBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { GearIcon } from './commons/icon/GearIcon';
import { TrackersStillOnModal } from './TrackersStillOnModal';
import { useConfig } from '@/hooks/config';
import { TrayOrExitModal } from './TrayOrExitModal';
import { ErrorConsentModal } from './ErrorConsentModal';
import { useAtomValue } from 'jotai';
import { connectedIMUTrackersAtom } from '@/store/app-store';
import { useElectron } from '@/hooks/electron';
Expand Down Expand Up @@ -286,11 +285,6 @@ export function TopBar({
setConnectedTrackerWarning(false);
}}
/>
<ErrorConsentModal
isOpen={config?.errorTracking === null}
accept={() => setConfig({ errorTracking: true })}
cancel={() => setConfig({ errorTracking: false })}
/>
</>
);
}
118 changes: 118 additions & 0 deletions gui/src/components/onboarding/UdevRulesModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/commons/Button';
import { BaseModal } from '@/components/commons/BaseModal';
import { CheckboxInternal } from '@/components/commons/Checkbox';
import { Typography } from '@/components/commons/Typography';
import { useElectron } from '@/hooks/electron';
import { useWebsocketAPI } from '@/hooks/websocket-api';
import { RpcMessage, InstalledInfoResponseT } from 'solarxr-protocol';
import { useConfig } from '@/hooks/config';

export function UdevRulesModal() {
const { config, setConfig } = useConfig();
const { useRPCPacket, sendRPCPacket } = useWebsocketAPI();
const electron = useElectron();
const [udevContent, setUdevContent] = useState('');
const [isUdevInstalledResponse, setIsUdevInstalledResponse] = useState(false);
const [showUdevWarning, setShowUdevWarning] = useState(false);
const [dontShowThisSession, setDontShowThisSession] = useState(false);
const [dontShowAgain, setDontShowAgain] = useState(false);
const [exeDir, setExeDir] = useState('');

const handleUdevContent = async () => {
if (electron.isElectron) {
const dir = await electron.api.getInstallDir();
setExeDir(dir);
const rulesDir = `${exeDir}/69-slimevr-devices.rules`;
setUdevContent(
`cat ${rulesDir} | sudo tee /etc/udev/rules.d/69-slimevr-devices.rules pn>/dev/null`
);
}
};

useEffect(() => {
handleUdevContent();
}, [exeDir]);

useEffect(() => {
if (!config) throw 'Invalid state!';
if (electron.isElectron) {
const isLinux = electron.data().os.type === 'linux';
const udevMissing = !isUdevInstalledResponse;
const notHiddenGlobally = !config.dontShowUdevModal;
const notHiddenThisSession = !dontShowThisSession;
const shouldShow =
isLinux && udevMissing && notHiddenGlobally && notHiddenThisSession;
if (shouldShow) {
setShowUdevWarning(true);
} else {
setShowUdevWarning(false);
}
}
}, [config, isUdevInstalledResponse, dontShowThisSession]);

useEffect(() => {
sendRPCPacket(
RpcMessage.InstalledInfoRequest,
new InstalledInfoResponseT()
);
}, []);

useRPCPacket(
RpcMessage.InstalledInfoResponse,
({ isUdevInstalled }: InstalledInfoResponseT) => {
setIsUdevInstalledResponse(isUdevInstalled);
}
);

const handleModalCose = () => {
if (!config) throw 'Invalid State!';
setConfig({ dontShowUdevModal: dontShowAgain });
setDontShowThisSession(true);
};

const copyToClipboard = () => {
navigator.clipboard.writeText(udevContent);
};

return (
<BaseModal isOpen={showUdevWarning} appendClasses={'w-full max-w-2xl'}>
<div className="flex w-full h-full flex-col gap-4">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<Typography
variant="main-title"
id="install-info_udev-rules_modal_title"
/>
<Typography id="install-info_udev-rules_warning" />
</div>
<div className="relative w-full max-w-2xl">
<div className="absolute right-2 top-2">
<Button variant="secondary" onClick={copyToClipboard}>
Copy
</Button>
</div>
<div className="bg-background-80 rounded-lg overflow-auto p-2 w-full h-[300px]">
<pre className="text-wrap">{udevContent}</pre>
</div>
</div>
</div>
<div className="flex justify-between gap-2">
<CheckboxInternal
label="Don't show this again"
outlined={false}
name={'dismiss-udev-rules-checkbox'}
loading={false}
disabled={false}
onChange={(e) => setDontShowAgain(e.currentTarget.checked)}
/>
<Button
variant="primary"
onClick={handleModalCose}
id="install-info_udev-rules_modal_button"
/>
</div>
</div>
</BaseModal>
);
}
51 changes: 51 additions & 0 deletions gui/src/components/onboarding/pages/ErrorCollectingConstent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Localized, useLocalization } from '@fluent/react';
import { Typography } from '@/components/commons/Typography';
import { Button } from '@/components/commons/Button';
import { useConfig } from '@/hooks/config';

export function ErrorCollectingConsentPage() {
const { setConfig } = useConfig();

const accept = () => {
setConfig({ errorTracking: true });
};

const cancel = () => {
setConfig({ errorTracking: false });
};

const { l10n } = useLocalization();
return (
<div className="flex items-center justify-center h-full flex-col gap-3 p-4">
<div className="max-w-2xl flex flex-col gap-4">
<div className="flex flex-col w-full gap-4">
<Typography variant="main-title" id="error_collection_modal-title" />
<Localized
id={'error_collection_modal-description_v2'}
elems={{
b: <b />,
h1: <span className="text-md font-bold" />,
}}
>
<Typography variant="standard" whitespace="whitespace-pre-line" />
</Localized>
</div>
<div className={'flex flex-row gap-2 justify-between'}>
<Button
variant="tertiary"
to="/onboarding/wifi-creds"
onClick={cancel}
id="error_collection_modal-cancel"
/>
<Button
variant="primary"
to="/onboarding/wifi-creds"
onClick={accept}
>
{l10n.getString('error_collection_modal-confirm')}
</Button>
</div>
</div>
</div>
);
}
2 changes: 1 addition & 1 deletion gui/src/components/onboarding/pages/Home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function HomePage() {
<Typography variant="mobile-title">
{l10n.getString('onboarding-home')}
</Typography>
<Button variant="primary" to="/onboarding/wifi-creds">
<Button variant="primary" to="/onboarding/error-collecting-consent">
{l10n.getString('onboarding-home-start')}
</Button>
</div>
Expand Down
2 changes: 2 additions & 0 deletions gui/src/hooks/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export interface Config {
homeLayout: 'default' | 'table';
skeletonPreview: boolean;
lastUsedProportions: 'manual' | 'autobone' | 'scaled' | null;
dontShowUdevModal: boolean;
}

export interface ConfigContext {
Expand Down Expand Up @@ -79,6 +80,7 @@ export const defaultConfig: Config = {
homeLayout: 'default',
skeletonPreview: true,
lastUsedProportions: null,
dontShowUdevModal: false,
};

const localStore: CrossStorage = {
Expand Down
3 changes: 2 additions & 1 deletion gui/src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ body {
}

@font-face {
src: url('/fonts/noto-sans-v42-latin-regular.woff2') format('woff2-variations');
src: url('/fonts/noto-sans-v42-latin-regular.woff2')
format('woff2-variations');
font-family: 'Noto Sans';
font-style: normal;
font-weight: 400;
Expand Down
6 changes: 5 additions & 1 deletion gui/src/utils/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import { DeviceDataT } from 'solarxr-protocol';

export function getSentryOrCompute(enabled = false, uuid: string) {
Sentry.setUser({ id: uuid });

// if sentry is already initialized - SKIP
if (enabled && Sentry.isInitialized()) return;
if (enabled && Sentry.isInitialized()) {
log('Sentry already enabled, skipping initialization');
return;
}

const client = Sentry.getClient();
if (client) {
Expand Down
Loading
Loading