diff --git a/src/App.browser.test.tsx b/src/App.browser.test.tsx index 73261db49..388d2ee8f 100644 --- a/src/App.browser.test.tsx +++ b/src/App.browser.test.tsx @@ -27,7 +27,8 @@ describe('Demo mode', () => { test('View demo route', async () => { const { findByText, findByTestId } = renderApp(`/${Demo.DONGLE_ID}/${DEMO_LOG_ID}`) - expect(await findByText(DEMO_LOG_ID)).toBeTruthy() + // Route ID includes the current segment, which starts at 0 + expect(await findByText(`${DEMO_LOG_ID}/0`)).toBeTruthy() const video = (await findByTestId('route-video')) as HTMLVideoElement await waitFor(() => expect(video.src).toBeTruthy()) }) @@ -42,7 +43,7 @@ describe.skip('Public routes', () => { test('View public route without signing in', async () => { const { findByText } = renderApp(`/${Demo.DONGLE_ID}/${DEMO_LOG_ID}`) - expect(await findByText(DEMO_LOG_ID)).toBeTruthy() + expect(await findByText(`${DEMO_LOG_ID}/0`)).toBeTruthy() // Videos do not load, yet // const video = (await findByTestId('route-video')) as HTMLVideoElement // await waitFor(() => expect(video.src).toBeTruthy()) diff --git a/src/components/RouteActions.tsx b/src/components/RouteActions.tsx index 632735914..f065bb752 100644 --- a/src/components/RouteActions.tsx +++ b/src/components/RouteActions.tsx @@ -5,6 +5,7 @@ import { USERADMIN_URL } from '~/api/config' import { setRoutePublic, setRoutePreserved, getPreservedRoutes, parseRouteName } from '~/api/route' import Icon from '~/components/material/Icon' import type { Route } from '~/api/types' +import { getRouteSegment } from '~/utils/format' const ToggleButton: VoidComponent<{ label: string @@ -35,6 +36,7 @@ const ToggleButton: VoidComponent<{ interface RouteActionsProps { routeName: string route: Route | undefined + seekTime: number } const RouteActions: VoidComponent = (props) => { @@ -43,7 +45,10 @@ const RouteActions: VoidComponent = (props) => { const [isPublic, setIsPublic] = createSignal(undefined) const [isPreserved, setIsPreserved] = createSignal(undefined) - const useradminUrl = () => `${USERADMIN_URL}/?onebox=${currentRouteId()}` + const routeInfo = () => parseRouteName(props.routeName) + const segment = () => getRouteSegment(props.seekTime, props.route?.maxqlog) + + const useradminUrl = () => `${USERADMIN_URL}/?onebox=${routeInfo().dongleId}/${routeInfo().routeId}` createEffect(() => { const preservedRoutes = preservedRoutesResource() @@ -88,7 +93,7 @@ const RouteActions: VoidComponent = (props) => { } } - const currentRouteId = () => props.routeName.replace('|', '/') + const currentRouteId = () => `${routeInfo().dongleId}/${routeInfo().routeId}/${segment()}` const copyCurrentRouteId = async () => { if (!props.routeName || !navigator.clipboard) return @@ -116,8 +121,10 @@ const RouteActions: VoidComponent = (props) => { class="flex w-full cursor-pointer items-center justify-between rounded-lg border-2 border-surface-container-high bg-surface-container-lowest p-3 hover:bg-surface-container-low" >
- {currentRouteId().split('/')[0] || ''}/ - {currentRouteId().split('/')[1] || ''} + {routeInfo().dongleId}/ + + {routeInfo().routeId}/{segment()} +
diff --git a/src/pages/dashboard/activities/RouteActivity.tsx b/src/pages/dashboard/activities/RouteActivity.tsx index 78ad9e5b3..98ffba9ed 100644 --- a/src/pages/dashboard/activities/RouteActivity.tsx +++ b/src/pages/dashboard/activities/RouteActivity.tsx @@ -87,7 +87,7 @@ const RouteActivity: VoidComponent = (props) => {
- +
diff --git a/src/utils/format.spec.ts b/src/utils/format.spec.ts index 3cbced8c6..a6fe73411 100644 --- a/src/utils/format.spec.ts +++ b/src/utils/format.spec.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { dateTimeToColorBetween, formatDate, formatDistance, formatDuration } from './format' +import { dateTimeToColorBetween, formatDate, formatDistance, formatDuration, getRouteSegment } from './format' describe('formatDistance', () => { it('should format distance', () => { @@ -25,6 +25,23 @@ describe('formatDuration', () => { }) }) +describe('getRouteSegment', () => { + it('should map a playback time to its segment number', () => { + expect(getRouteSegment(0)).toBe(0) + expect(getRouteSegment(59.9)).toBe(0) + expect(getRouteSegment(60)).toBe(1) + expect(getRouteSegment(125)).toBe(2) + }) + it('should never return a negative segment', () => { + expect(getRouteSegment(-10)).toBe(0) + }) + it('should clamp to the last available segment', () => { + expect(getRouteSegment(120, 5)).toBe(2) + expect(getRouteSegment(600, 3)).toBe(3) + expect(getRouteSegment(60, 0)).toBe(0) + }) +}) + describe('formatDate', () => { beforeEach(() => { vi.useFakeTimers() diff --git a/src/utils/format.ts b/src/utils/format.ts index 5929085a5..c249cde13 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -50,6 +50,16 @@ export const formatVideoTime = (seconds: number): string => { return `${minutes}:${remainingSeconds.padStart(2, '0')}` } +// openpilot splits every route into fixed-length segments +export const SEGMENT_LENGTH = 60 // seconds + +// Map a playback time (in seconds) to its segment number, clamped to [0, maxSegment] +export const getRouteSegment = (seekTime: number, maxSegment?: number): number => { + const segment = Math.max(0, Math.floor(seekTime / SEGMENT_LENGTH)) + if (maxSegment === undefined) return segment + return Math.min(segment, Math.max(0, maxSegment)) +} + export const getRouteDuration = (route: Route | undefined): Duration | undefined => { if (!route || !route.start_time || !route.end_time) return undefined const startTime = dayjs(route.start_time)