Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 15 additions & 17 deletions src/components/RouteActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { createSignal, Show, type VoidComponent, createEffect, createResource }
import clsx from 'clsx'

import { USERADMIN_URL } from '~/api/config'
import { setRoutePublic, setRoutePreserved, getPreservedRoutes, parseRouteName } from '~/api/route'
import { setRoutePublic, setRoutePreserved, getPreservedRoutes } from '~/api/route'
import Icon from '~/components/material/Icon'
import type { Route } from '~/api/types'
import { currentRoute } from '~/store'

const ToggleButton: VoidComponent<{
label: string
Expand Down Expand Up @@ -32,13 +32,8 @@ const ToggleButton: VoidComponent<{
</button>
)

interface RouteActionsProps {
routeName: string
route: Route | undefined
}

const RouteActions: VoidComponent<RouteActionsProps> = (props) => {
const [preservedRoutesResource] = createResource(() => parseRouteName(props.routeName).dongleId, getPreservedRoutes)
const RouteActions: VoidComponent = () => {
const [preservedRoutesResource] = createResource(() => currentRoute()?.dongle_id, getPreservedRoutes)

const [isPublic, setIsPublic] = createSignal<boolean | undefined>(undefined)
const [isPreserved, setIsPreserved] = createSignal<boolean | undefined>(undefined)
Expand All @@ -47,12 +42,13 @@ const RouteActions: VoidComponent<RouteActionsProps> = (props) => {

createEffect(() => {
const preservedRoutes = preservedRoutesResource()
if (!props.route) return
setIsPublic(props.route.is_public)
if (props.route.is_preserved) {
const route = currentRoute()
if (!route) return
setIsPublic(route.is_public)
if (route.is_preserved) {
setIsPreserved(true)
} else if (preservedRoutes) {
const { fullname } = props.route
const { fullname } = route
setIsPreserved(preservedRoutes.some((r) => r.fullname === fullname))
} else {
setIsPreserved(undefined)
Expand All @@ -64,12 +60,14 @@ const RouteActions: VoidComponent<RouteActionsProps> = (props) => {

const toggleRoute = async (property: 'public' | 'preserved') => {
setError(null)
const route = currentRoute()
if (!route) return
if (property === 'public') {
const currentValue = isPublic()
if (currentValue === undefined) return
try {
const newValue = !currentValue
await setRoutePublic(props.routeName, newValue)
await setRoutePublic(route.fullname, newValue)
setIsPublic(newValue)
} catch (err) {
console.error('Failed to update public toggle', err)
Expand All @@ -81,7 +79,7 @@ const RouteActions: VoidComponent<RouteActionsProps> = (props) => {

try {
const newValue = !currentValue
await setRoutePreserved(props.routeName, newValue)
await setRoutePreserved(route.fullname, newValue)
setIsPreserved(newValue)
} catch (err) {
console.error('Failed to update preserved toggle', err)
Expand All @@ -90,10 +88,10 @@ const RouteActions: VoidComponent<RouteActionsProps> = (props) => {
}
}

const currentRouteId = () => props.routeName.replace('|', '/')
const currentRouteId = () => currentRoute()?.fullname?.replace('|', '/') || ''

const copyCurrentRouteId = async () => {
if (!props.routeName || !navigator.clipboard) return
if (!currentRoute()?.fullname || !navigator.clipboard) return

try {
await navigator.clipboard.writeText(currentRouteId())
Expand Down
5 changes: 2 additions & 3 deletions src/components/RouteStaticMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import clsx from 'clsx'
import { GPSPathPoint, getCoords } from '~/api/derived'
import { Coords, getPathStaticMapUrl } from '~/map'
import { getThemeId } from '~/theme'
import type { Route } from '~/api/types'
import { currentRoute } from '~/store'

import Icon from '~/components/material/Icon'

Expand Down Expand Up @@ -48,11 +48,10 @@ const State = (props: {

type RouteStaticMapProps = {
class?: string
route: Route | undefined
}

const RouteStaticMap: VoidComponent<RouteStaticMapProps> = (props) => {
const [coords] = createResource(() => props.route, getCoords)
const [coords] = createResource(() => currentRoute(), getCoords)
const [url] = createResource(coords, getStaticMapUrl)
const [loadedUrl] = createResource(url, loadImage)

Expand Down
8 changes: 4 additions & 4 deletions src/components/RouteStatistics.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import type { VoidComponent } from 'solid-js'

import type { TimelineStatistics } from '~/api/derived'
import type { Route } from '~/api/types'
import { formatDistance, formatRouteDuration } from '~/utils/format'
import StatisticBar from './StatisticBar'
import { currentRoute } from '~/store'

const formatEngagement = (timeline: TimelineStatistics | undefined): string | undefined => {
if (!timeline || timeline.duration === 0) return undefined
const { engagedDuration, duration } = timeline
return `${(100 * (engagedDuration / duration)).toFixed(0)}%`
}

const RouteStatistics: VoidComponent<{ class?: string; route: Route | undefined; timeline: TimelineStatistics | undefined }> = (props) => {
const RouteStatistics: VoidComponent<{ class?: string; timeline: TimelineStatistics | undefined }> = (props) => {
return (
<StatisticBar
class={props.class}
statistics={[
{ label: 'Distance', value: () => formatDistance(props.route?.length) },
{ label: 'Duration', value: () => (props.route ? formatRouteDuration(props.route) : undefined) },
{ label: 'Distance', value: () => formatDistance(currentRoute()?.length) },
{ label: 'Duration', value: () => (currentRoute() ? formatRouteDuration(currentRoute()) : undefined) },
{ label: 'Engaged', value: () => formatEngagement(props.timeline) },
]}
/>
Expand Down
15 changes: 6 additions & 9 deletions src/components/RouteUploadButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import clsx from 'clsx'
import Icon, { type IconName } from '~/components/material/Icon'
import Button from './material/Button'
import { uploadAllSegments, type FileType } from '~/api/file'
import type { Route } from '~/api/types'
import { currentRoute } from '~/store'

const BUTTON_TYPES = ['road', 'driver', 'logs', 'route']
type ButtonType = (typeof BUTTON_TYPES)[number]
Expand Down Expand Up @@ -54,11 +54,7 @@ const UploadButton: VoidComponent<UploadButtonProps> = (props) => {
)
}

interface RouteUploadButtonsProps {
route: Route | undefined
}

const RouteUploadButtons: VoidComponent<RouteUploadButtonsProps> = (props) => {
const RouteUploadButtons: VoidComponent = () => {
const [uploadStore, setUploadStore] = createStore({
states: {
road: 'idle',
Expand All @@ -71,7 +67,7 @@ const RouteUploadButtons: VoidComponent<RouteUploadButtonsProps> = (props) => {

createEffect(
on(
() => props.route,
() => currentRoute(),
() => {
abortController().abort()
setAbortController(new AbortController())
Expand All @@ -81,8 +77,9 @@ const RouteUploadButtons: VoidComponent<RouteUploadButtonsProps> = (props) => {
)

const handleUpload = async (type: ButtonType) => {
if (!props.route) return
const { fullname, maxqlog } = props.route
const route = currentRoute()
if (!route) return
const { fullname, maxqlog } = route
const { signal } = abortController()

const updateButtonStates = (types: readonly ButtonType[], state: ButtonState) => {
Expand Down
7 changes: 3 additions & 4 deletions src/components/RouteVideoPlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import { getQCameraStreamUrl } from '~/api/route'
import IconButton from '~/components/material/IconButton'
import { formatVideoTime } from '~/utils/format'
import type Hls from '~/utils/hls'
import { currentRoute } from '~/store'

type RouteVideoPlayerProps = {
class?: string
routeName: string
selection: { startTime: number; endTime: number | undefined }
onProgress: (seekTime: number) => void
ref: (el?: HTMLVideoElement) => void
Expand All @@ -18,8 +18,7 @@ const ERROR_MISSING_SEGMENT = 'This video segment has not uploaded yet or has be
const ERROR_UNSUPPORTED_BROWSER = 'This browser does not support Media Source Extensions API.'

const RouteVideoPlayer: VoidComponent<RouteVideoPlayerProps> = (props) => {
const routeName = () => props.routeName
const [streamUrl] = createResource(routeName, getQCameraStreamUrl)
const [streamUrl] = createResource(() => currentRoute()?.fullname, getQCameraStreamUrl)
const [hls, setHls] = createSignal<Hls | null>()
let video!: HTMLVideoElement
let controls!: HTMLDivElement
Expand Down Expand Up @@ -140,7 +139,7 @@ const RouteVideoPlayer: VoidComponent<RouteVideoPlayerProps> = (props) => {

// State reset on route change
createEffect(
on(routeName, () => {
on(currentRoute, () => {
setVideoLoading(true)
setErrorMessage('')
}),
Expand Down
14 changes: 7 additions & 7 deletions src/components/Timeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import type { VoidComponent } from 'solid-js'
import clsx from 'clsx'

import type { TimelineEvent } from '~/api/derived'
import type { Route } from '~/api/types'
import { getRouteDuration } from '~/utils/format'
import { currentRoute } from '~/store'

function renderTimelineEvents(route: Route | undefined, events: TimelineEvent[]) {
function renderTimelineEvents(events: TimelineEvent[]) {
const route = currentRoute()
if (!route) return
const duration = getRouteDuration(route)?.asMilliseconds() ?? 0
return (
Expand Down Expand Up @@ -91,14 +92,13 @@ const MARKER_WIDTH = 3

interface TimelineProps {
class?: string
route: Route | undefined
seekTime: number
updateTime: (time: number) => void
events: TimelineEvent[]
}

const Timeline: VoidComponent<TimelineProps> = (props) => {
const route = () => props.route
const route = () => currentRoute()
// TODO: align to first camera frame event
const [markerOffsetPct, setMarkerOffsetPct] = createSignal(0)
const [duration] = createResource(route, (route) => getRouteDuration(route)?.asSeconds() ?? 0, { initialValue: 0 })
Expand Down Expand Up @@ -139,13 +139,13 @@ const Timeline: VoidComponent<TimelineProps> = (props) => {
}

const onMouseDown = (ev: MouseEvent) => {
if (!props.route) return
if (!currentRoute()) return
updateMarker(ev.clientX)
onStart()
}

const onTouchStart = (ev: TouchEvent) => {
if (ev.touches.length !== 1 || !props.route) return
if (ev.touches.length !== 1 || !currentRoute()) return
updateMarker(ev.touches[0].clientX)
onStart()
}
Expand Down Expand Up @@ -177,7 +177,7 @@ const Timeline: VoidComponent<TimelineProps> = (props) => {
title="Disengaged"
>
<div class="absolute inset-0 size-full rounded-b-md overflow-hidden">
<Suspense fallback={<div class="skeleton-loader size-full" />}>{renderTimelineEvents(props.route, props.events)}</Suspense>
<Suspense fallback={<div class="skeleton-loader size-full" />}>{renderTimelineEvents(props.events)}</Suspense>
</div>
<div
class="absolute top-0 z-10 h-full"
Expand Down
6 changes: 2 additions & 4 deletions src/pages/dashboard/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,8 @@ const Dashboard: Component<RouteSectionProps> = () => {
<Match when={urlState().dateStr === 'settings' || urlState().dateStr === 'prime'}>
<SettingsActivity dongleId={dongleId} />
</Match>
<Match when={urlState().dateStr} keyed>
{(dateStr) => (
<RouteActivity dongleId={dongleId} dateStr={dateStr} startTime={urlState().startTime} endTime={urlState().endTime} />
)}
<Match when={urlState().dateStr}>
{(dateStr) => <RouteActivity dateStr={dateStr()} startTime={urlState().startTime} endTime={urlState().endTime} />}
</Match>
</Switch>
}
Expand Down
34 changes: 15 additions & 19 deletions src/pages/dashboard/activities/DeviceActivity.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import clsx from 'clsx'
import { createResource, createSignal, For, Show, Suspense } from 'solid-js'
import { createSignal, For, Show, Suspense } from 'solid-js'
import type { VoidComponent } from 'solid-js'

import { getDevice, SHARED_DEVICE } from '~/api/devices'
import { SHARED_DEVICE } from '~/api/devices'
import { currentDevice } from '~/store'
import { ATHENA_URL } from '~/api/config'
import { getAccessToken } from '~/api/auth/client'

Expand All @@ -29,12 +30,9 @@ interface SnapshotResponse {
}

const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
// TODO: device should be passed in from DeviceList
const [device] = createResource(() => props.dongleId, getDevice)
// Resource as source of another resource blocks component initialization
const deviceName = () => (device.latest ? getDeviceName(device.latest) : '')
const deviceName = () => (currentDevice() ? getDeviceName(currentDevice()) : '')
// TODO: remove this. if we're listing the routes for a device you should always be a user, this is for viewing public routes which are being removed
const isDeviceUser = () => (device.loading ? true : device.latest?.is_owner || device.latest?.alias !== SHARED_DEVICE)
const isDeviceUser = () => currentDevice()?.is_owner || currentDevice()?.alias !== SHARED_DEVICE
const [queueVisible, setQueueVisible] = createSignal(false)
const [snapshot, setSnapshot] = createSignal<{
error: string | null
Expand Down Expand Up @@ -127,18 +125,16 @@ const DeviceActivity: VoidComponent<DeviceActivityProps> = (props) => {
<DeviceLocation dongleId={props.dongleId} deviceName={deviceName()!} />
</Suspense>
<div class="flex items-center justify-between p-4">
<Suspense fallback={<div class="h-[32px] skeleton-loader size-full rounded-xs" />}>
<div class="inline-flex items-center gap-2">
<div
class={clsx(
'm-2 size-2 shrink-0 rounded-full',
device.latest && deviceIsOnline(device.latest) ? 'bg-green-400' : 'bg-gray-400',
)}
/>

{<div class="text-xl font-bold">{deviceName()}</div>}
</div>
</Suspense>
<div class="inline-flex items-center gap-2">
<div
class={clsx(
'm-2 size-2 shrink-0 rounded-full',
currentDevice() && deviceIsOnline(currentDevice()) ? 'bg-green-400' : 'bg-gray-400',
)}
/>

{<div class="text-xl font-bold">{deviceName()}</div>}
</div>
<div class="flex gap-4">
<IconButton name="camera" onClick={() => void takeSnapshot()} />
<IconButton name="settings" href={`/${props.dongleId}/settings`} />
Expand Down
Loading
Loading