mobile swappy controls

This commit is contained in:
legop3
2026-03-23 02:04:21 -04:00
parent f112f74db0
commit 626b48f777
8 changed files with 70 additions and 32 deletions
+2
View File
@@ -8,6 +8,8 @@ This means that locking / unlocking will act a little different than standard ro
- private status is defined in the roverd config - private status is defined in the roverd config
- needs to never leak through access to anyone while locked - needs to never leak through access to anyone while locked
- unlocking a private rover is a big deal for verified users (opening up a rover in the main living space for a special event) - unlocking a private rover is a big deal for verified users (opening up a rover in the main living space for a special event)
- not included in LLM events system
- basically needs to be online but completely hidden when its not open
## locking / unlocking: ## locking / unlocking:
- private rovers start locked - private rovers start locked
+2 -1
View File
@@ -9,7 +9,8 @@
- audio files first [x] - audio files first [x]
- then voice chat [x] - then voice chat [x]
8. mobile controls column swapping (optional joystick on left) 8. mobile controls column swapping (optional joystick on left)
9. home assistant rover mute switch 9. fix fullscreen on mobile so that you can re-enter it
10. home assistant rover mute switch
# relative pipe dreams: # relative pipe dreams:
1. VPS video forwarding 1. VPS video forwarding
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -11,7 +11,7 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" /> <meta name="apple-mobile-web-app-title" content="Multi Roomba Rover" />
<title>Multi Roomba Rover</title> <title>Multi Roomba Rover</title>
<script type="module" crossorigin src="/assets/index-8-JHgslZ.js"></script> <script type="module" crossorigin src="/assets/index-Cf-6drTv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B-TLXecO.css"> <link rel="stylesheet" crossorigin href="/assets/index-B-TLXecO.css">
</head> </head>
<body> <body>
+20 -10
View File
@@ -3,8 +3,8 @@ import TelemetryPanel from './components/TelemetryPanel.jsx';
import ReplaySourcesPanel from './components/ReplaySourcesPanel.jsx'; import ReplaySourcesPanel from './components/ReplaySourcesPanel.jsx';
import AlertFeed from './components/AlertFeed.jsx'; import AlertFeed from './components/AlertFeed.jsx';
import MobileControls, { import MobileControls, {
MobileLeftColumn, MobileActionsColumn,
MobileRightColumn, MobileDriveColumn,
} from './components/MobileControls.jsx'; } from './components/MobileControls.jsx';
import { import {
ControlSystemProvider, ControlSystemProvider,
@@ -160,11 +160,11 @@ function MobileFeatureTabs({
); );
} }
function MobilePortraitLayout({ onOpenHelpOverlay }) { function MobilePortraitLayout({ onOpenHelpOverlay, swapMobileControlColumns = false }) {
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<DriverVideoPanel layoutFormat="mobile-portrait" /> <DriverVideoPanel layoutFormat="mobile-portrait" />
<MobileControls /> <MobileControls swapColumns={swapMobileControlColumns} />
<div className="grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]"> <div className="grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
<ReplaySourcesPanel panelId="replay-sources-mobile-portrait" /> <ReplaySourcesPanel panelId="replay-sources-mobile-portrait" />
<RoverQueuesPanel /> <RoverQueuesPanel />
@@ -179,12 +179,18 @@ function MobilePortraitLayout({ onOpenHelpOverlay }) {
); );
} }
function MobileLandscapeLayout({ onOpenHelpOverlay }) { function MobileLandscapeLayout({ onOpenHelpOverlay, swapMobileControlColumns = false }) {
const columnClass = 'self-start h-[min(100svh,32rem)]'; const columnClass = 'self-start h-[min(100svh,32rem)]';
const firstColumn = swapMobileControlColumns
? <MobileDriveColumn layout="landscape" className={columnClass} />
: <MobileActionsColumn layout="landscape" className={columnClass} />;
const secondColumn = swapMobileControlColumns
? <MobileActionsColumn layout="landscape" className={columnClass} />
: <MobileDriveColumn layout="landscape" className={columnClass} />;
return ( return (
<div className="flex flex-col gap-0.5"> <div className="flex flex-col gap-0.5">
<section className="grid min-h-screen grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] gap-0.5"> <section className="grid min-h-screen grid-cols-[minmax(0,0.7fr)_minmax(0,2.1fr)_minmax(0,0.7fr)] gap-0.5">
<MobileLeftColumn layout="landscape" className={columnClass} /> {firstColumn}
<div> <div>
<DriverVideoPanel layoutFormat="mobile-landscape" /> <DriverVideoPanel layoutFormat="mobile-landscape" />
<div className="grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]"> <div className="grid gap-0.5 grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)]">
@@ -193,7 +199,7 @@ function MobileLandscapeLayout({ onOpenHelpOverlay }) {
</div> </div>
{/* <TelemetryPanel /> */} {/* <TelemetryPanel /> */}
</div> </div>
<MobileRightColumn layout="landscape" className={columnClass} /> {secondColumn}
</section> </section>
<div className="flex flex-col gap-0.5 pb-0"> <div className="flex flex-col gap-0.5 pb-0">
<MobileFeatureTabs <MobileFeatureTabs
@@ -234,6 +240,10 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
status: helpStatus, status: helpStatus,
save: saveHelpSettings, save: saveHelpSettings,
} = useSettingsNamespace('help', { showOnLoad: true }); } = useSettingsNamespace('help', { showOnLoad: true });
const { value: pageSettings } = useSettingsNamespace('page', {
swapMobileControlColumns: false,
});
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
const [helpVisible, setHelpVisible] = useState(false); const [helpVisible, setHelpVisible] = useState(false);
useEffect(() => { useEffect(() => {
@@ -260,9 +270,9 @@ function AppWithProviders({ layout, isDesktop, fullscreen }) {
isDesktop isDesktop
? <DesktopLayout layout={layout} onOpenHelpOverlay={openHelp} /> ? <DesktopLayout layout={layout} onOpenHelpOverlay={openHelp} />
: layout === 'mobile-landscape' : layout === 'mobile-landscape'
? <MobileLandscapeLayout onOpenHelpOverlay={openHelp} /> ? <MobileLandscapeLayout onOpenHelpOverlay={openHelp} swapMobileControlColumns={swapMobileControlColumns} />
: <MobilePortraitLayout onOpenHelpOverlay={openHelp} />, : <MobilePortraitLayout onOpenHelpOverlay={openHelp} swapMobileControlColumns={swapMobileControlColumns} />,
[isDesktop, layout, openHelp], [isDesktop, layout, openHelp, swapMobileControlColumns],
); );
return ( return (
+13 -7
View File
@@ -224,7 +224,7 @@ function MobileAuxButton({ id, label, values, color, disabled, onPress, onReleas
); );
} }
function MobileLeftColumnContent({ layout }) { function MobileActionsColumnContent({ layout }) {
const { const {
state: { roverId, camera, horn }, state: { roverId, camera, horn },
pipeline, pipeline,
@@ -347,15 +347,15 @@ function MobileLeftColumnContent({ layout }) {
); );
} }
export function MobileLeftColumn({ layout, className = '' }) { export function MobileActionsColumn({ layout, className = '' }) {
return ( return (
<div className={`flex flex-col gap-0.5 ${className}`.trim()}> <div className={`flex flex-col gap-0.5 ${className}`.trim()}>
<MobileLeftColumnContent layout={layout} /> <MobileActionsColumnContent layout={layout} />
</div> </div>
); );
} }
export function MobileRightColumn({ layout, className = '' }) { export function MobileDriveColumn({ layout, className = '' }) {
return ( return (
<div className={`flex flex-col gap-0.5 ${className}`.trim()}> <div className={`flex flex-col gap-0.5 ${className}`.trim()}>
<MobileJoystickPanel layout={layout === 'landscape' ? 'landscape' : 'portrait'} /> <MobileJoystickPanel layout={layout === 'landscape' ? 'landscape' : 'portrait'} />
@@ -363,13 +363,19 @@ export function MobileRightColumn({ layout, className = '' }) {
); );
} }
export default function MobilePortraitControls() { export default function MobilePortraitControls({ swapColumns = false }) {
const columnHeight = 'h-[min(60svh,24rem)]'; const columnHeight = 'h-[min(60svh,24rem)]';
const firstColumn = swapColumns
? <MobileDriveColumn layout="portrait" className={columnHeight} />
: <MobileActionsColumn layout="portrait" className={columnHeight} />;
const secondColumn = swapColumns
? <MobileActionsColumn layout="portrait" className={columnHeight} />
: <MobileDriveColumn layout="portrait" className={columnHeight} />;
return ( return (
<section className="panel"> <section className="panel">
<div className="grid grid-cols-2 gap-0.5 items-stretch"> <div className="grid grid-cols-2 gap-0.5 items-stretch">
<MobileLeftColumn layout="portrait" className={columnHeight} /> {firstColumn}
<MobileRightColumn layout="portrait" className={columnHeight} /> {secondColumn}
</div> </div>
</section> </section>
); );
+19
View File
@@ -32,9 +32,11 @@ export default function SettingsPanel() {
const { value: pageSettings, save: savePageSettings } = useSettingsNamespace('page', { const { value: pageSettings, save: savePageSettings } = useSettingsNamespace('page', {
hudMapDesktop: false, hudMapDesktop: false,
connectionTransport: 'websocket', connectionTransport: 'websocket',
swapMobileControlColumns: false,
}); });
const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS); const { value: audioSettings, save: saveAudioSettings } = useSettingsNamespace('audio', AUDIO_SETTINGS_DEFAULTS);
const connectionTransport = pageSettings?.connectionTransport || 'websocket'; const connectionTransport = pageSettings?.connectionTransport || 'websocket';
const swapMobileControlColumns = Boolean(pageSettings?.swapMobileControlColumns);
const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume; const masterVolume = Number.isFinite(audioSettings?.masterVolume) ? audioSettings.masterVolume : AUDIO_SETTINGS_DEFAULTS.masterVolume;
const alertVolume = Number.isFinite(audioSettings?.alertVolume) ? audioSettings.alertVolume : AUDIO_SETTINGS_DEFAULTS.alertVolume; const alertVolume = Number.isFinite(audioSettings?.alertVolume) ? audioSettings.alertVolume : AUDIO_SETTINGS_DEFAULTS.alertVolume;
const roverVolume = Number.isFinite(audioSettings?.roverVolume) ? audioSettings.roverVolume : AUDIO_SETTINGS_DEFAULTS.roverVolume; const roverVolume = Number.isFinite(audioSettings?.roverVolume) ? audioSettings.roverVolume : AUDIO_SETTINGS_DEFAULTS.roverVolume;
@@ -80,6 +82,11 @@ export default function SettingsPanel() {
const checked = Boolean(event.target.checked); const checked = Boolean(event.target.checked);
saveAudioSettings((current) => ({ ...(current ?? {}), mainBrushDuckEnabled: checked })); saveAudioSettings((current) => ({ ...(current ?? {}), mainBrushDuckEnabled: checked }));
}; };
const handleSwapMobileControlColumns = (event) => {
const checked = Boolean(event.target.checked);
savePageSettings((current) => ({ ...(current ?? {}), swapMobileControlColumns: checked }));
};
return ( return (
<Tabs defaultTab="keybindings"> <Tabs defaultTab="keybindings">
<TabList> <TabList>
@@ -114,6 +121,18 @@ export default function SettingsPanel() {
</label> </label>
<p className="text-xs text-slate-500">Mobile HUD keeps the map on by default.</p> <p className="text-xs text-slate-500">Mobile HUD keeps the map on by default.</p>
</section> </section>
<section className="panel-section space-y-0.5 text-sm">
<p className="text-slate-400">Mobile controls</p>
<label className="flex items-center gap-0.5 text-slate-200">
<input
type="checkbox"
className="accent-emerald-500"
checked={swapMobileControlColumns}
onChange={handleSwapMobileControlColumns}
/>
<span>Swap control columns (put joystick on the left)</span>
</label>
</section>
<section className="panel-section space-y-0.5 text-sm"> <section className="panel-section space-y-0.5 text-sm">
<p className="text-slate-400">Audio</p> <p className="text-slate-400">Audio</p>
<label className="grid gap-0.5 text-slate-200"> <label className="grid gap-0.5 text-slate-200">
+2 -2
View File
@@ -124,7 +124,7 @@ export const HELP_CONTENT = {
title: 'Driving the rover', title: 'Driving the rover',
items: [ items: [
{ segments: ['Press the "Start Driving" button onscreen to put the rover into driving mode.'] }, { segments: ['Press the "Start Driving" button onscreen to put the rover into driving mode.'] },
'Look below the rover video. Use the joystick on the right to move the rover, and hold the buttons on the left to run the aux motors.' 'Look below the rover video. Use the joystick column to move the rover, and hold the aux buttons in the other control column.'
], ],
}, },
{ {
@@ -154,7 +154,7 @@ export const HELP_CONTENT = {
title: 'Driving the rover', title: 'Driving the rover',
items: [ items: [
{ segments: ['Press the "Start Driving" button, or the "Drive" button to put the rover into driving mode.'] }, { segments: ['Press the "Start Driving" button, or the "Drive" button to put the rover into driving mode.'] },
'Use the joystick to the right of the video feed to move the rover, and hold the buttons on the left to run the aux motors.' 'Use the joystick column beside the video feed to move the rover, and hold the aux buttons in the other control column.'
], ],
}, },
{ {