diff --git a/src/components/App.tsx b/src/components/App.tsx index ba384f5d..575cf1cd 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -15,7 +15,7 @@ import { ViewerState, clearUrlHash } from "@/lib/ViewerState"; import { TrackManager, loadTrackManager } from "@/lib/TrackManager"; import { PointSelectionMode } from "@/lib/PointSelector"; import LeftSidebarWrapper from "./leftSidebar/LeftSidebarWrapper"; -import { TimestampOverlay } from "./overlays/TimestampOverlay"; +// import { TimestampOverlay } from "./overlays/TimestampOverlay"; import { ColorMap } from "./overlays/ColorMap"; import { TrackDownloadData } from "./DownloadButton"; @@ -29,6 +29,56 @@ const initialViewerState = ViewerState.fromUrlHash(window.location.hash); console.log("initial viewer state: ", initialViewerState); clearUrlHash(); +function detectDeviceType(): { isPhone: boolean; isTablet: boolean; isMobile: boolean } { + const ua = navigator.userAgent || navigator.vendor; + + // Detect iPads, iPhones, and iPods based on the user agent string + const isiPad = + /iPad/.test(ua) || (navigator.maxTouchPoints && navigator.maxTouchPoints > 1 && /Macintosh/.test(ua)); + const isiPhoneOrIPod = /iPhone|iPod/.test(ua); + + // Detect Android phones and tablets + const isAndroidPhone = /Android/.test(ua) && /Mobile/.test(ua); + const isAndroidTablet = /Android/.test(ua) && !/Mobile/.test(ua); + + // Screen size check (tablets typically have a wider screen) + const isSmallScreen = window.screen.width <= 768; + const hasTouch = navigator.maxTouchPoints > 1; + + // Determine if it's a phone, tablet, or desktop + const isPhone = isiPhoneOrIPod || isAndroidPhone || isSmallScreen; + const isTablet = isiPad || isAndroidTablet || hasTouch; + const isDesktop = !isPhone && !isTablet; // It's a desktop if it's neither a phone nor a tablet + + // manually asign labels for debugging + // const isPhone = false; + // const isTablet = true; + // const isDesktop = false; + + return { + isPhone: isPhone, + isTablet: isTablet && !isPhone, // To avoid small phones being miscategorized as tablets + isMobile: !isDesktop, + }; +} + +export const detectedDevice = detectDeviceType(); +console.debug("detectDeviceType: ", detectedDevice); +if (detectedDevice.isPhone) { + window.confirm("Note: for full functionality, please use a tablet or desktop device. Press 'OK' to continue "); +} + +// for debugging: show the detected device type in an alert +// window.confirm( +// "detected device type (desktop | tablet | phone) = (" + +// !detectDeviceType().isMobile + +// " | " + +// detectDeviceType().isTablet + +// " | " + +// detectDeviceType().isPhone + +// ")", +// ); + const drawerWidth = 256; const playbackFPS = 16; const playbackIntervalMs = 1000 / playbackFPS; @@ -298,128 +348,155 @@ export default function App() { }; return ( - + {/* TODO: components *could* go deeper still for organization */} - - - {brandingLogoPath && } - {brandingLogoPath && brandingName && } - {brandingName &&

{brandingName}

}{" "} -
- - {/* Scrollable section for other controls */} - - { - dispatchCanvas({ type: ActionType.REMOVE_ALL_TRACKS }); + { - dispatchCanvas({ type: ActionType.POINT_BRIGHTNESS, brightness }); + > + {brandingLogoPath && } + {brandingLogoPath && brandingName && } + {brandingName &&

{brandingName}

}{" "} +
+ { - dispatchCanvas({ type: ActionType.POINT_SIZES, pointSize }); - }} - selectionMode={canvas.selector.selectionMode} - setSelectionMode={(value: PointSelectionMode) => { - dispatchCanvas({ type: ActionType.SELECTION_MODE, selectionMode: value }); - }} - /> - - 0} - trackManager={trackManager} - trackHighlightLength={trackHighlightLength} - selectionMode={canvas.selector.selectionMode} - showTracks={canvas.showTracks} - setShowTracks={(show: boolean) => { - dispatchCanvas({ type: ActionType.SHOW_TRACKS, showTracks: show }); - }} - showTrackHighlights={canvas.showTrackHighlights} - setShowTrackHighlights={(show: boolean) => { - dispatchCanvas({ type: ActionType.SHOW_TRACK_HIGHLIGHTS, showTrackHighlights: show }); - }} - setTrackHighlightLength={(length: number) => { - dispatchCanvas({ - type: ActionType.MIN_MAX_TIME, - minTime: canvas.curTime - length / 2, - maxTime: canvas.curTime + length / 2, - }); - }} - /> + > + { + dispatchCanvas({ type: ActionType.REMOVE_ALL_TRACKS }); + }} + getTrackDownloadData={getTrackDownloadData} + numSelectedCells={numSelectedCells} + numSelectedTracks={numSelectedTracks} + trackManager={trackManager} + pointBrightness={canvas.pointBrightness} + setPointBrightness={(brightness: number) => { + dispatchCanvas({ type: ActionType.POINT_BRIGHTNESS, brightness }); + }} + pointSize={canvas.pointSize} + setPointSize={(pointSize: number) => { + dispatchCanvas({ type: ActionType.POINT_SIZES, pointSize }); + }} + selectionMode={canvas.selector.selectionMode} + setSelectionMode={(value: PointSelectionMode) => { + dispatchCanvas({ type: ActionType.SELECTION_MODE, selectionMode: value }); + }} + isTablet={detectedDevice.isTablet} + MobileSelectCells={() => { + dispatchCanvas({ type: ActionType.MOBILE_SELECT_CELLS }); + }} + setSelectorScale={(scale: number) => { + dispatchCanvas({ type: ActionType.SELECTOR_SCALE, scale }); + }} + selectorScale={canvas.selector.sphereSelector.cursor.scale.x} + /> + + 0} + trackManager={trackManager} + trackHighlightLength={trackHighlightLength} + selectionMode={canvas.selector.selectionMode} + showTracks={canvas.showTracks} + setShowTracks={(show: boolean) => { + dispatchCanvas({ type: ActionType.SHOW_TRACKS, showTracks: show }); + }} + showTrackHighlights={canvas.showTrackHighlights} + setShowTrackHighlights={(show: boolean) => { + dispatchCanvas({ + type: ActionType.SHOW_TRACK_HIGHLIGHTS, + showTrackHighlights: show, + }); + }} + setTrackHighlightLength={(length: number) => { + dispatchCanvas({ + type: ActionType.MIN_MAX_TIME, + minTime: canvas.curTime - length / 2, + maxTime: canvas.curTime + length / 2, + }); + }} + isTablet={detectedDevice.isTablet} + /> + + + + +
- - - - -
-
+ + )} + {/* Box for Scene + playBackControls */} - 0} /> - - + {/* The canvas (Scene + colormap + timestamp) */} + + 0} /> + {/* */} + + + {/* The playback controls */} + void; pointSize: number; setPointSize: (value: number) => void; - selectionMode: PointSelectionMode; + selectionMode: PointSelectionMode | null; setSelectionMode: (value: PointSelectionMode) => void; + isTablet: boolean; + MobileSelectCells: () => void; + setSelectorScale: (value: number) => void; + selectorScale: number; } export default function CellControls(props: CellControlsProps) { const buttonDefinition: SingleButtonDefinition[] = [ { icon: "Cube", tooltipText: "Box", value: PointSelectionMode.BOX }, - { icon: "Starburst", tooltipText: "Spherical cursor", value: PointSelectionMode.SPHERICAL_CURSOR }, - { icon: "Globe", tooltipText: "Sphere", value: PointSelectionMode.SPHERE }, + { icon: "Starburst", tooltipText: "Sphere", value: PointSelectionMode.SPHERICAL_CURSOR }, + { icon: "Globe", tooltipText: "Adjustable sphere", value: PointSelectionMode.SPHERE }, ]; + // Intercept onChange of selection buttons to prevent the first two buttons from being selected on mobile devices + const handleSegmentedControlChange = (_e: React.MouseEvent, newValue: PointSelectionMode | null) => { + // If isTablet is true and the selected value corresponds to the first or second button, do nothing + if ( + props.isTablet && + (newValue === PointSelectionMode.BOX || newValue === PointSelectionMode.SPHERICAL_CURSOR) + ) { + window.alert("This selection mode is not available on mobile devices."); + console.log("Mobile device detected, preventing selection of box or spherical cursor"); + return; // Prevent selection + } + props.setSelectionMode(newValue!); // Otherwise, update the selection mode + }; + return ( @@ -49,12 +67,41 @@ export default function CellControls(props: CellControlsProps) { { - props.setSelectionMode(v); - }} + onChange={handleSegmentedControlChange} value={props.selectionMode} /> + + {props.isTablet && props.selectionMode === PointSelectionMode.SPHERE && ( + + )} + + {(props.selectionMode === PointSelectionMode.SPHERICAL_CURSOR || + props.selectionMode === PointSelectionMode.SPHERE) && + props.isTablet && ( + <> +
+ +
+ `${value}`} + onChange={(_, value) => { + props.setSelectorScale(value as number); + }} + value={props.selectorScale} + /> + + )} {numberOfValuesPerPoint !== 4 && ( <>