|
|
@ -1,22 +1,38 @@ |
|
|
import { useState, useMemo, useCallback } from "react"; |
|
|
import { useCallback, useMemo, useState } from "react"; |
|
|
import type { Protobuf } from "@meshtastic/core"; |
|
|
import type { Protobuf } from "@meshtastic/core"; |
|
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; |
|
|
|
|
|
|
|
|
export type FilterValue = |
|
|
interface BooleanFilter { |
|
|
| boolean |
|
|
key: string; |
|
|
| [number, number] |
|
|
label: string; |
|
|
| string[] |
|
|
type: "boolean"; |
|
|
| string; |
|
|
predicate: (node: Node, value: boolean) => boolean; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
export interface FilterConfig<T extends FilterValue = FilterValue> { |
|
|
interface RangeFilter { |
|
|
key: string; |
|
|
key: string; |
|
|
label: string; |
|
|
label: string; |
|
|
type: "boolean" | "range" | "search"; |
|
|
type: "range"; |
|
|
bounds?: [number, number]; |
|
|
bounds: [number, number]; |
|
|
options?: string[]; |
|
|
predicate: (node: Node, value: [number, number]) => boolean; |
|
|
predicate: (node: Protobuf.Mesh.NodeInfo, value: T) => boolean; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
interface SearchFilter { |
|
|
|
|
|
key: string; |
|
|
|
|
|
label: string; |
|
|
|
|
|
type: "search"; |
|
|
|
|
|
predicate: (node: Node, value: string) => boolean; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
export type FilterConfig = BooleanFilter | RangeFilter | SearchFilter; |
|
|
|
|
|
|
|
|
|
|
|
export type FilterValueMap = { |
|
|
|
|
|
[C in FilterConfig as C["key"]]: C extends BooleanFilter ? boolean |
|
|
|
|
|
: C extends RangeFilter ? [number, number] |
|
|
|
|
|
: C extends SearchFilter ? string |
|
|
|
|
|
: never; |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
// Defines all node filters in this object
|
|
|
// Defines all node filters in this object
|
|
|
export const filterConfigs: FilterConfig[] = [ |
|
|
export const filterConfigs: FilterConfig[] = [ |
|
|
{ |
|
|
{ |
|
|
@ -30,7 +46,9 @@ export const filterConfigs: FilterConfig[] = [ |
|
|
const nodeNum = node.num?.toString() ?? ""; |
|
|
const nodeNum = node.num?.toString() ?? ""; |
|
|
const nodeNumHex = numberToHexUnpadded(node.num) ?? ""; |
|
|
const nodeNumHex = numberToHexUnpadded(node.num) ?? ""; |
|
|
const search = text.toLowerCase(); |
|
|
const search = text.toLowerCase(); |
|
|
return shortName.includes(search) || longName.includes(search) || nodeNum.includes(search) || nodeNumHex.includes(search.replace(/!/g, "")); |
|
|
return shortName.includes(search) || longName.includes(search) || |
|
|
|
|
|
nodeNum.includes(search) || |
|
|
|
|
|
nodeNumHex.includes(search.replace(/!/g, "")); |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
{ |
|
|
{ |
|
|
@ -84,11 +102,11 @@ export const filterConfigs: FilterConfig[] = [ |
|
|
label: "Hide MQTT-connected nodes", |
|
|
label: "Hide MQTT-connected nodes", |
|
|
type: "boolean", |
|
|
type: "boolean", |
|
|
predicate: (node, hide: boolean) => !hide || !node.viaMqtt, |
|
|
predicate: (node, hide: boolean) => !hide || !node.viaMqtt, |
|
|
} |
|
|
}, |
|
|
]; |
|
|
]; |
|
|
|
|
|
|
|
|
export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
const defaultState = useMemo<Record<string, FilterValue>>(() => { |
|
|
const defaultState = useMemo(() => { |
|
|
return filterConfigs.reduce((acc, cfg) => { |
|
|
return filterConfigs.reduce((acc, cfg) => { |
|
|
switch (cfg.type) { |
|
|
switch (cfg.type) { |
|
|
case "boolean": |
|
|
case "boolean": |
|
|
@ -102,31 +120,37 @@ export function useNodeFilters(nodes: Protobuf.Mesh.NodeInfo[]) { |
|
|
break; |
|
|
break; |
|
|
} |
|
|
} |
|
|
return acc; |
|
|
return acc; |
|
|
}, {} as Record<string, FilterValue>); |
|
|
}, {} as FilterValueMap); |
|
|
}, []); |
|
|
}, []); |
|
|
|
|
|
|
|
|
const [filters, setFilters] = useState<Record<string, FilterValue>>(defaultState); |
|
|
const [filters, setFilters] = useState<FilterValueMap>( |
|
|
|
|
|
defaultState, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
const resetFilters = useCallback(() => { |
|
|
const resetFilters = useCallback(() => { |
|
|
setFilters(defaultState); |
|
|
setFilters(defaultState); |
|
|
}, [defaultState]); |
|
|
}, [defaultState]); |
|
|
|
|
|
|
|
|
const onFilterChange = useCallback( |
|
|
const onFilterChange = useCallback( |
|
|
(key: string, value: FilterValue) => { |
|
|
<K extends keyof FilterValueMap>(key: K, value: FilterValueMap[K]) => { |
|
|
setFilters((f) => ({ ...f, [key]: value })); |
|
|
setFilters((f) => ({ ...f, [key]: value })); |
|
|
}, |
|
|
}, |
|
|
[] |
|
|
[], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
const filteredNodes = useMemo( |
|
|
const filteredNodes = useMemo( |
|
|
() => |
|
|
() => |
|
|
nodes.filter((node) => |
|
|
nodes.filter((node) => |
|
|
filterConfigs.every((cfg) => |
|
|
filterConfigs.every((cfg) => cfg.predicate(node, filters[cfg.key])) |
|
|
cfg.predicate(node, filters[cfg.key]) |
|
|
|
|
|
) |
|
|
|
|
|
), |
|
|
), |
|
|
[nodes, filters] |
|
|
[nodes, filters], |
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
return { filters, onFilterChange, resetFilters, filteredNodes, filterConfigs }; |
|
|
return { |
|
|
|
|
|
filters, |
|
|
|
|
|
onFilterChange, |
|
|
|
|
|
resetFilters, |
|
|
|
|
|
filteredNodes, |
|
|
|
|
|
filterConfigs, |
|
|
|
|
|
}; |
|
|
} |
|
|
} |