Fix Search, no jumpy video

This commit is contained in:
dirwin31
2026-08-27 17:23:32 -07:00
parent f5ba6362ce
commit 3c852bdbb0
3 changed files with 75 additions and 8 deletions
@@ -1254,7 +1254,8 @@
}
.dashcam-video-shell {
aspect-ratio: 16 / 9;
/* Match qcamera.ts so the visible frame does not widen during the full-quality swap. */
aspect-ratio: 526 / 330;
background: #07080b;
overflow: hidden;
position: relative;
@@ -6,6 +6,16 @@ function validDate(value) {
return Number.isNaN(date.getTime()) ? null : date
}
export function normalizeRouteSearchText(value) {
return String(value || "")
.normalize("NFKD")
.replace(/\p{M}/gu, "")
.toLocaleLowerCase()
.replace(/(\d+)(?:st|nd|rd|th)\b/g, "$1")
.replace(/[^\p{L}\p{N}]+/gu, " ")
.trim()
}
export function formatRouteDate(value, locale) {
const date = validDate(value)
if (!date) return "Unknown date"
@@ -19,8 +29,17 @@ export function normalizeRoute(route, locale) {
const timestamp = route?.timestamp == null ? null : String(route.timestamp)
const startedAtDate = validDate(route?.startedAt)
const timestampDate = validDate(timestamp)
const displayDate = formatRouteDate(startedAtDate || timestampDate, locale)
const routeDate = startedAtDate || timestampDate
const displayDate = formatRouteDate(routeDate, locale)
const isCustomName = Boolean(route?.isCustomName) || Boolean(timestamp && !timestampDate)
const displayName = isCustomName ? timestamp : displayDate
const dateAliases = routeDate ? [
new Intl.DateTimeFormat(locale, { dateStyle: "long" }).format(routeDate),
new Intl.DateTimeFormat("en-US", { month: "long", day: "numeric", year: "numeric" }).format(routeDate),
new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", year: "numeric" }).format(routeDate),
`${routeDate.getMonth() + 1}/${routeDate.getDate()}/${routeDate.getFullYear()}`,
`${routeDate.getFullYear()}-${routeDate.getMonth() + 1}-${routeDate.getDate()}`,
] : []
return {
...route,
@@ -29,8 +48,13 @@ export function normalizeRoute(route, locale) {
startedAt: route?.startedAt || null,
isCustomName,
displayDate,
displayName: isCustomName ? timestamp : displayDate,
displayName,
_startedAtMs: startedAtDate?.getTime() ?? timestampDate?.getTime() ?? null,
_searchValues: [
normalizeRouteSearchText(route?.name),
normalizeRouteSearchText(isCustomName ? displayName : ""),
...dateAliases.map(normalizeRouteSearchText),
].filter(Boolean),
}
}
@@ -96,11 +120,19 @@ export function sortRoutes(routes, sortOrder = "newest") {
}
export function routeMatchesSearch(route, searchQuery) {
const query = String(searchQuery || "").trim().toLocaleLowerCase()
if (!query) return true
return [route?.name, route?.timestamp, route?.displayName, route?.displayDate]
.filter(Boolean)
.some(value => String(value).toLocaleLowerCase().includes(query))
const queryTokens = normalizeRouteSearchText(searchQuery).split(" ").filter(Boolean)
if (!queryTokens.length) return true
const searchValues = Array.isArray(route?._searchValues) ? route._searchValues : [
route?.name,
route?.timestamp,
route?.displayName,
route?.displayDate,
].map(normalizeRouteSearchText).filter(Boolean)
return searchValues.some(value => {
const searchTokens = value.split(" ").filter(Boolean)
return queryTokens.every(queryToken => searchTokens.some(searchToken => searchToken.includes(queryToken)))
})
}
export function buildRouteView(routes, options = {}) {
@@ -15,6 +15,7 @@ import pytest
HELPERS_PATH = Path(__file__).resolve().parent.parent / "assets" / "components" / "recordings" / "dashcam_routes_helpers.js"
COMPONENT_PATH = HELPERS_PATH.with_name("dashcam_routes.js")
COMPONENT_CSS_PATH = HELPERS_PATH.with_name("dashcam_routes.css")
# node infers ESM from `export` syntax in a bare .js file from 22.7 on, so the helpers
# need no package.json and stay a normal asset next to the component that imports them.
@@ -78,6 +79,12 @@ def test_route_titles_and_custom_name_badges_are_reactive():
assert source.count('${() => route.isCustomName ? html`') >= 4
def test_player_shell_matches_low_quality_video_aspect_ratio():
source = COMPONENT_CSS_PATH.read_text(encoding="utf-8")
assert "aspect-ratio: 526 / 330;" in source
def test_groups_routes_into_today_yesterday_dates_and_unknown():
groups = evaluate('''
const routes = [
@@ -129,6 +136,33 @@ def test_searches_custom_names_displayed_dates_and_route_ids():
assert matches == [1, 1, 1, 0]
def test_search_matches_partial_title_tokens_and_friendly_dates_as_the_user_types():
result = evaluate('''
const routes = [
route("0000006a--9f0a7bdf9c", "2026-08-27T08:00:00Z", { timestamp: "Test_31", isCustomName: true }),
route("0000006b--9f0a7bdf9d", "2026-08-28T08:00:00Z", { timestamp: "Morning drive", isCustomName: true }),
route("0000006c--9f0a7bdf9e", "2026-09-27T08:00:00Z", { timestamp: "Test_4", isCustomName: true }),
]
return ["test", "test 3", "aug", "aug 2", "aug 27th", "8/27"]
.map(searchQuery => buildRouteView(routes, { searchQuery }).matching.map(item => item.name))
''')
assert result == [
["0000006c--9f0a7bdf9e", "0000006a--9f0a7bdf9c"],
["0000006a--9f0a7bdf9c"],
["0000006b--9f0a7bdf9d", "0000006a--9f0a7bdf9c"],
["0000006b--9f0a7bdf9d", "0000006a--9f0a7bdf9c"],
["0000006a--9f0a7bdf9c"],
["0000006a--9f0a7bdf9c"],
]
def test_route_search_input_updates_on_every_keystroke():
source = COMPONENT_PATH.read_text(encoding="utf-8")
assert '@input="${event => { state.searchQuery = event.target.value }}"' in source
def test_filters_preserved_routes_before_applying_the_render_limit():
view = evaluate('''
const routes = Array.from({ length: MAX_RENDERED_ROUTES + 25 }, (_, index) => route(