mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
better um interinstance ui um stuff yeah lole
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -78,8 +78,8 @@
|
||||
<script defer src="https://analytics.otter.land/script.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land"></script>
|
||||
<script defer src="https://analytics.otter.land/recorder.js" data-website-id="82dd56a5-db44-4279-bd1e-a4d9fee39af7" data-domains="rover.otter.land" data-sample-rate="0.15" data-mask-level="moderate" data-max-duration="300000"></script>
|
||||
<title>Roomba Rover</title>
|
||||
<script type="module" crossorigin src="/assets/index-D7t7G3Qn.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DzqCFmyF.css">
|
||||
<script type="module" crossorigin src="/assets/index-IknCIkGO.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-9ecW8jwy.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
1. assign rovers based on battery percentage, give people highest one
|
||||
2. add admin ui for VIP and private requests instead of only through discord
|
||||
3. make overcurrent limiter speed sensitive, slower fill at lower speeds
|
||||
1. interinstance UI updates
|
||||
1. on desktop, scale up the overlay UI, like actually scale like 1.5x
|
||||
2. update the rover queues panel so that instances are always expanded
|
||||
1. have a "scroll for more" thing
|
||||
2. make it so that the list takes up all the vertical space
|
||||
2. assign rovers based on battery percentage, give people highest one
|
||||
3. add admin ui for VIP and private requests instead of only through discord
|
||||
4. add more background gap themes
|
||||
5. fix this:
|
||||
`Jun 18 15:14:18 roombaserver.local node[216731]: /home/daniel/MultiRoombaRover/server/src/services/roverManager/socketHandlers.js:92
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Inter Instance Panel
|
||||
// Purpose: Renders remote rover servers discovered through the inter-instance directory.
|
||||
// Scope: Owns external server metadata presentation while reusing RoverQueuesPanel for rover/queue rows.
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useSessionSelector } from '../../context/SessionContext.jsx';
|
||||
import CardFrame from '../CardFrame/index.jsx';
|
||||
import RoverQueuesPanel from '../RoverQueuesPanel/index.jsx';
|
||||
@@ -133,44 +133,99 @@ function RemoteMediaStrip({ remote }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollableInstanceList({ children }) {
|
||||
const viewportRef = useRef(null);
|
||||
const contentRef = useRef(null);
|
||||
const [canScrollDown, setCanScrollDown] = useState(false);
|
||||
|
||||
const measureScrollRemainder = useCallback(() => {
|
||||
const viewport = viewportRef.current;
|
||||
if (!viewport) return;
|
||||
|
||||
/*
|
||||
A small tolerance prevents fractional browser measurements from leaving
|
||||
the cue visible when the user is effectively at the bottom. Comparing the
|
||||
live viewport and content dimensions also means the cue only appears when
|
||||
there is genuinely hidden content, rather than merely because several
|
||||
instances happen to exist.
|
||||
*/
|
||||
const remaining = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
|
||||
setCanScrollDown(remaining > 2);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const viewport = viewportRef.current;
|
||||
const content = contentRef.current;
|
||||
if (!viewport || !content) return undefined;
|
||||
|
||||
/*
|
||||
Remote rosters and queues can change height without a window resize. A
|
||||
ResizeObserver on both the viewport and its inner content keeps the cue
|
||||
accurate for those live session updates while avoiding polling timers.
|
||||
*/
|
||||
const observer = new ResizeObserver(measureScrollRemainder);
|
||||
observer.observe(viewport);
|
||||
observer.observe(content);
|
||||
const animationFrame = window.requestAnimationFrame(measureScrollRemainder);
|
||||
|
||||
return () => {
|
||||
window.cancelAnimationFrame(animationFrame);
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [measureScrollRemainder]);
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
ref={viewportRef}
|
||||
className="min-h-0 flex-1 overflow-y-auto"
|
||||
onScroll={measureScrollRemainder}
|
||||
>
|
||||
<div ref={contentRef} className={classNames('space-y-0.5', canScrollDown && 'pb-6')}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
{canScrollDown ? (
|
||||
<div className="pointer-events-none absolute inset-x-0 bottom-0 bg-gradient-to-t from-neutral-950 via-neutral-950/90 to-transparent px-1 pb-0.5 pt-5 text-center text-xs font-semibold text-slate-200">
|
||||
Scroll for more ↓
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExternalInstancesCompact() {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [popupOpen, setPopupOpen] = useState(false);
|
||||
const enabled = useInterInstanceEnabled();
|
||||
const instances = useRemoteInstances();
|
||||
const visible = useMemo(() => instances.filter((remote) => remote?.online || remote?.url), [instances]);
|
||||
if (!enabled) return null;
|
||||
if (!visible.length) return null;
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
<div className="grid grid-cols-2 gap-0.5">
|
||||
<button type="button" className="button-dark w-full" onClick={() => setExpanded((value) => !value)}>
|
||||
{expanded ? 'Hide external' : `Show external (${visible.length})`}
|
||||
</button>
|
||||
<button type="button" className="button-dark w-full" onClick={() => setPopupOpen(true)}>
|
||||
Browse servers
|
||||
</button>
|
||||
</div>
|
||||
{expanded ? (
|
||||
<div className="space-y-0.5">
|
||||
{visible.map((remote) =>
|
||||
remote.online ? (
|
||||
<RoverQueuesPanel
|
||||
key={remote.url}
|
||||
title={remote.instance?.name || remote.url}
|
||||
roster={remote.roster}
|
||||
turnQueues={remote.turnQueues}
|
||||
users={remote.users}
|
||||
externalInstance={remote}
|
||||
disabledOverlay={getRemoteAvailability(remote).blocked ? getRemoteAvailability(remote).overlay : ''}
|
||||
/>
|
||||
) : (
|
||||
<InstancePanel key={remote.url} remote={remote} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
{popupOpen ? <InterInstancePopup onClose={() => setPopupOpen(false)} /> : null}
|
||||
/*
|
||||
External instances are intentionally always mounted. Besides removing an
|
||||
unnecessary disclosure click, this preserves the live queue rows while
|
||||
the local Rover Queues card uses this region as its remaining-height
|
||||
scroller. The viewport cap remains a safety boundary in layouts whose
|
||||
parent has natural height instead of a fixed desktop row height.
|
||||
*/
|
||||
<div className="flex min-h-0 max-h-[min(60vh,36rem)] flex-1 flex-col border-t border-neutral-600/60 pt-0.5">
|
||||
<ScrollableInstanceList>
|
||||
{visible.map((remote) =>
|
||||
remote.online ? (
|
||||
<RoverQueuesPanel
|
||||
key={remote.url}
|
||||
title={remote.instance?.name || remote.url}
|
||||
roster={remote.roster}
|
||||
turnQueues={remote.turnQueues}
|
||||
users={remote.users}
|
||||
externalInstance={remote}
|
||||
disabledOverlay={getRemoteAvailability(remote).blocked ? getRemoteAvailability(remote).overlay : ''}
|
||||
/>
|
||||
) : (
|
||||
<InstancePanel key={remote.url} remote={remote} />
|
||||
),
|
||||
)}
|
||||
</ScrollableInstanceList>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -180,8 +235,9 @@ export function InterInstancePopup({ onClose }) {
|
||||
<div className="fixed inset-0 z-[70] flex items-center justify-center bg-black/80 p-0.5">
|
||||
<InterInstanceBrowserFrame
|
||||
onClose={onClose}
|
||||
className="max-w-[calc(100vw-0.5rem)]"
|
||||
bodyClassName="max-h-[82vh] overflow-y-auto p-0.5"
|
||||
scaledOverlay
|
||||
className="inter-instance-overlay-frame"
|
||||
bodyClassName="inter-instance-overlay-body overflow-y-auto p-0.5"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -226,6 +282,7 @@ export function InterInstanceBrowserFrame({
|
||||
className = '',
|
||||
bodyClassName = 'p-0.5',
|
||||
centered = false,
|
||||
scaledOverlay = false,
|
||||
}) {
|
||||
const enabled = useInterInstanceEnabled();
|
||||
const instances = useRemoteInstances();
|
||||
@@ -245,7 +302,7 @@ export function InterInstanceBrowserFrame({
|
||||
<CardFrame
|
||||
title="External instances"
|
||||
actions={actions}
|
||||
className={className}
|
||||
className={classNames(scaledOverlay && 'inter-instance-overlay-scale', className)}
|
||||
bodyClassName={bodyClassName}
|
||||
clipOverflow={false}
|
||||
>
|
||||
|
||||
@@ -102,8 +102,9 @@ export default function ModeGateOverlay() {
|
||||
*/
|
||||
<InterInstanceBrowserFrame
|
||||
hideWhenEmpty
|
||||
className="max-w-[calc(100vw-0.5rem)]"
|
||||
bodyClassName="max-h-[86vh] overflow-y-auto p-0.5"
|
||||
scaledOverlay
|
||||
className="inter-instance-overlay-frame"
|
||||
bodyClassName="inter-instance-overlay-body overflow-y-auto p-0.5"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -197,8 +197,13 @@ function QueueReplayLinksRow() {
|
||||
*/
|
||||
return (
|
||||
<div className={`flex ${themeGapClass}`}>
|
||||
<div className={`min-w-0 basis-0 grow-[1] space-y-0.5`}>
|
||||
<RoverQueuesPanel />
|
||||
<div className="min-w-0 basis-0 grow-[1]">
|
||||
{/*
|
||||
The queue card stretches to the desktop row height so its always-open
|
||||
external-instance region receives the same vertical budget as the
|
||||
neighboring replay card and can scroll within that space.
|
||||
*/}
|
||||
<RoverQueuesPanel fillHeight />
|
||||
</div>
|
||||
<div className="min-w-0 basis-0 grow-[0.9]">
|
||||
<ReplaySourcesPanel panelId="replay-sources-desktop" fillHeight />
|
||||
|
||||
@@ -8,7 +8,7 @@ import CardFrame from '../CardFrame/index.jsx';
|
||||
import QueueTargetRow from '../QueueTargetRow/index.jsx';
|
||||
import { trackAnalyticsEvent } from '../../analytics/index.js';
|
||||
import { openExternalRover } from '../../lib/interInstanceTransfer.js';
|
||||
import { ExternalInstancesCompact } from '../InterInstancePanel/index.jsx';
|
||||
import { ExternalInstancesCompact, InterInstancePopup } from '../InterInstancePanel/index.jsx';
|
||||
import { isFeatureEnabled } from '../../lib/features.js';
|
||||
import { useSettingsNamespace } from '../../settings/index.js';
|
||||
|
||||
@@ -32,12 +32,16 @@ export default function RoverQueuesPanel({
|
||||
users: usersOverride = null,
|
||||
externalInstance = null,
|
||||
disabledOverlay = '',
|
||||
fillHeight = false,
|
||||
}) {
|
||||
const role = useSessionSelector((state) => state.session?.role || null);
|
||||
const localRoster = useSessionSelector((state) => state.session?.roster ?? []);
|
||||
const localTurnQueues = useSessionSelector((state) => state.session?.turnQueues ?? {});
|
||||
const localUsers = useSessionSelector((state) => state.session?.users ?? []);
|
||||
const interInstanceEnabled = useSessionSelector((state) => isFeatureEnabled(state, 'interInstance'));
|
||||
const hasRemoteInstances = useSessionSelector(
|
||||
(state) => (state.session?.interInstances?.instances?.length ?? 0) > 0,
|
||||
);
|
||||
const { value: pageSettings } = useSettingsNamespace('page', { interInstanceTransferSettings: true });
|
||||
const selfId = useSessionSelector((state) => state.session?.socketId || null);
|
||||
const assignedRoverId = useSessionSelector((state) => String(state.session?.assignment?.roverId || '').trim());
|
||||
@@ -50,6 +54,7 @@ export default function RoverQueuesPanel({
|
||||
const { requestControl, rebootOwnRover } = useSessionActions();
|
||||
const [pending, setPending] = useState({});
|
||||
const [rebootPending, setRebootPending] = useState(false);
|
||||
const [interInstancePopupOpen, setInterInstancePopupOpen] = useState(false);
|
||||
const externalMode = Boolean(externalInstance);
|
||||
const externalBlocked = Boolean(externalMode && disabledOverlay);
|
||||
const includeInterInstanceSettings = pageSettings?.interInstanceTransferSettings !== false;
|
||||
@@ -146,8 +151,8 @@ export default function RoverQueuesPanel({
|
||||
}
|
||||
}
|
||||
|
||||
const headerActions =
|
||||
!externalMode && role !== 'spectator' && assignedRoverId ? (
|
||||
const rebootAction =
|
||||
role !== 'spectator' && assignedRoverId ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleRebootOwnRover}
|
||||
@@ -159,14 +164,35 @@ export default function RoverQueuesPanel({
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
const headerActions = !externalMode ? (
|
||||
<>
|
||||
{interInstanceEnabled && hasRemoteInstances ? (
|
||||
<button
|
||||
type="button"
|
||||
className="button-dark"
|
||||
onClick={() => setInterInstancePopupOpen(true)}
|
||||
>
|
||||
Browse servers
|
||||
</button>
|
||||
) : null}
|
||||
{rebootAction}
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<CardFrame title={title} actions={headerActions} bodyClassName="space-y-0.5 text-sm">
|
||||
<div className="relative space-y-0.5">
|
||||
{rosterItems.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">No rovers registered.</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5 text-sm">
|
||||
{rosterItems.map((rover) => {
|
||||
<>
|
||||
<CardFrame
|
||||
title={title}
|
||||
actions={headerActions}
|
||||
fillHeight={fillHeight}
|
||||
bodyClassName="space-y-0.5 text-sm"
|
||||
>
|
||||
<div className={fillHeight ? 'relative flex min-h-0 flex-1 flex-col gap-0.5' : 'relative space-y-0.5'}>
|
||||
{rosterItems.length === 0 ? (
|
||||
<p className="text-sm text-slate-500">No rovers registered.</p>
|
||||
) : (
|
||||
<ul className="space-y-0.5 text-sm">
|
||||
{rosterItems.map((rover) => {
|
||||
const roverId = String(rover.id);
|
||||
const info = turnQueues?.[roverId] || null;
|
||||
const queue = info?.queue || [];
|
||||
@@ -220,16 +246,25 @@ export default function RoverQueuesPanel({
|
||||
showAction={Boolean(canRequest)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
{externalBlocked ? (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-black/70 px-2 text-center text-sm font-semibold text-slate-100">
|
||||
{disabledOverlay}
|
||||
</div>
|
||||
) : null}
|
||||
{!externalMode && interInstanceEnabled ? <ExternalInstancesCompact /> : null}
|
||||
</div>
|
||||
</CardFrame>
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
{externalBlocked ? (
|
||||
<div className="absolute inset-0 z-10 flex items-center justify-center rounded bg-black/70 px-2 text-center text-sm font-semibold text-slate-100">
|
||||
{disabledOverlay}
|
||||
</div>
|
||||
) : null}
|
||||
{!externalMode && interInstanceEnabled ? <ExternalInstancesCompact /> : null}
|
||||
</div>
|
||||
</CardFrame>
|
||||
{interInstancePopupOpen ? (
|
||||
/*
|
||||
The popup remains owned by the local Rover Queues panel because its
|
||||
title-bar action opens it. External queue panels never render that
|
||||
action, which prevents recursively opening browsers from remote rows.
|
||||
*/
|
||||
<InterInstancePopup onClose={() => setInterInstancePopupOpen(false)} />
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,6 +59,39 @@ body {
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.inter-instance-overlay-frame {
|
||||
/* The unscaled frame always stays inside the viewport on phones and on
|
||||
desktop browsers that do not apply the larger presentation below. */
|
||||
max-width: calc(100vw - 0.5rem);
|
||||
}
|
||||
|
||||
.inter-instance-overlay-body {
|
||||
max-height: 82vh;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.inter-instance-overlay-scale {
|
||||
/*
|
||||
`zoom` enlarges the complete interface—including typography, controls,
|
||||
spacing, and hit targets—while participating in layout. A transform
|
||||
would only enlarge the paint result and could overlap or clip sibling
|
||||
content because the browser would still reserve the original size.
|
||||
*/
|
||||
zoom: 1.5;
|
||||
}
|
||||
|
||||
.inter-instance-overlay-frame {
|
||||
/* Reserve the inverse width before the 1.5x zoom so the final rendered
|
||||
frame still fits inside the physical desktop viewport. */
|
||||
max-width: calc((100vw - 0.5rem) / 1.5);
|
||||
}
|
||||
|
||||
.inter-instance-overlay-body {
|
||||
/* 54.6667vh becomes approximately 82vh after the 1.5x desktop zoom. */
|
||||
max-height: 54.6667vh;
|
||||
}
|
||||
}
|
||||
|
||||
.pride-page-bg {
|
||||
background-color: #050505;
|
||||
background-image:
|
||||
|
||||
Reference in New Issue
Block a user