This commit is contained in:
Nick Mowen 2023-12-15 16:42:51 -07:00
parent 9bc67e9ab8
commit 70d61832fe
4 changed files with 98 additions and 39 deletions

View File

@ -39,7 +39,7 @@ export function Dashboard() {
{config && ( {config && (
<div> <div>
<div className="grid gap-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4"> <div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{sortedCameras.map((camera) => { {sortedCameras.map((camera) => {
return <Camera key={camera.name} camera={camera} />; return <Camera key={camera.name} camera={camera} />;
})} })}

View File

@ -26,6 +26,14 @@ function Live() {
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
return config?.cameras[camera]; return config?.cameras[camera];
}, [camera, config]); }, [camera, config]);
const sortedCameras = useMemo(() => {
if (!config) {
return [];
}
return Object.values(config.cameras)
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
}, [config]);
const restreamEnabled = useMemo(() => { const restreamEnabled = useMemo(() => {
return ( return (
config && config &&
@ -66,7 +74,7 @@ function Live() {
<DropdownMenuLabel>Select A Camera</DropdownMenuLabel> <DropdownMenuLabel>Select A Camera</DropdownMenuLabel>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuRadioGroup value={camera} onValueChange={setCamera}> <DropdownMenuRadioGroup value={camera} onValueChange={setCamera}>
{Object.keys(config?.cameras || {}).map((item) => ( {Object.keys(sortedCameras).map((item) => (
<DropdownMenuRadioItem <DropdownMenuRadioItem
className="capitalize" className="capitalize"
key={item} key={item}

View File

@ -6,6 +6,8 @@ export interface UiConfig {
strftime_fmt?: string; strftime_fmt?: string;
live_mode?: string; live_mode?: string;
use_experimental?: boolean; use_experimental?: boolean;
dashboard: boolean;
order: number;
} }
export interface CameraConfig { export interface CameraConfig {

View File

@ -1,6 +1,5 @@
import strftime from 'strftime'; import strftime from "strftime";
import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns'; import { fromUnixTime, intervalToDuration, formatDuration } from "date-fns";
import { UiConfig } from "@/types/frigateConfig";
export const longToDate = (long: number): Date => new Date(long * 1000); export const longToDate = (long: number): Date => new Date(long * 1000);
export const epochToLong = (date: number): number => date / 1000; export const epochToLong = (date: number): number => date / 1000;
export const dateToLong = (date: Date): number => epochToLong(date.getTime()); export const dateToLong = (date: Date): number => epochToLong(date.getTime());
@ -40,23 +39,45 @@ export const getNowYesterdayInLong = (): number => {
// only used as a fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat // only used as a fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
const formatMap: { const formatMap: {
[k: string]: { [k: string]: {
date: { year: 'numeric' | '2-digit'; month: 'long' | 'short' | '2-digit'; day: 'numeric' | '2-digit' }; date: {
time: { hour: 'numeric'; minute: 'numeric'; second?: 'numeric'; timeZoneName?: 'short' | 'long' }; year: "numeric" | "2-digit";
month: "long" | "short" | "2-digit";
day: "numeric" | "2-digit";
};
time: {
hour: "numeric";
minute: "numeric";
second?: "numeric";
timeZoneName?: "short" | "long";
};
}; };
} = { } = {
full: { full: {
date: { year: 'numeric', month: 'long', day: 'numeric' }, date: { year: "numeric", month: "long", day: "numeric" },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' }, time: {
hour: "numeric",
minute: "numeric",
second: "numeric",
timeZoneName: "long",
},
}, },
long: { long: {
date: { year: 'numeric', month: 'long', day: 'numeric' }, date: { year: "numeric", month: "long", day: "numeric" },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric', timeZoneName: 'long' }, time: {
hour: "numeric",
minute: "numeric",
second: "numeric",
timeZoneName: "long",
},
}, },
medium: { medium: {
date: { year: 'numeric', month: 'short', day: 'numeric' }, date: { year: "numeric", month: "short", day: "numeric" },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' }, time: { hour: "numeric", minute: "numeric", second: "numeric" },
},
short: {
date: { year: "2-digit", month: "2-digit", day: "2-digit" },
time: { hour: "numeric", minute: "numeric" },
}, },
short: { date: { year: '2-digit', month: '2-digit', day: '2-digit' }, time: { hour: 'numeric', minute: 'numeric' } },
}; };
/** /**
@ -79,11 +100,11 @@ const getResolvedTimeZone = () => {
return Intl.DateTimeFormat().resolvedOptions().timeZone; return Intl.DateTimeFormat().resolvedOptions().timeZone;
} catch (error) { } catch (error) {
const offsetMinutes = new Date().getTimezoneOffset(); const offsetMinutes = new Date().getTimezoneOffset();
return `UTC${offsetMinutes < 0 ? '+' : '-'}${Math.abs(offsetMinutes / 60) return `UTC${offsetMinutes < 0 ? "+" : "-"}${Math.abs(offsetMinutes / 60)
.toString() .toString()
.padStart(2, '0')}:${Math.abs(offsetMinutes % 60) .padStart(2, "0")}:${Math.abs(offsetMinutes % 60)
.toString() .toString()
.padStart(2, '0')}`; .padStart(2, "0")}`;
} }
}; };
@ -103,11 +124,21 @@ const getResolvedTimeZone = () => {
* *
* @throws {Error} If the given unixTimestamp is not a valid number, the function will return 'Invalid time'. * @throws {Error} If the given unixTimestamp is not a valid number, the function will return 'Invalid time'.
*/ */
export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: UiConfig): string => { export const formatUnixTimestampToDateTime = (
const { timezone, time_format, date_style, time_style, strftime_fmt } = config; unixTimestamp: number,
const locale = window.navigator?.language || 'en-US'; config: {
timezone?: string;
time_format?: "browser" | "12hour" | "24hour";
date_style?: "full" | "long" | "medium" | "short";
time_style?: "full" | "long" | "medium" | "short";
strftime_fmt?: string;
}
): string => {
const { timezone, time_format, date_style, time_style, strftime_fmt } =
config;
const locale = window.navigator?.language || "en-US";
if (isNaN(unixTimestamp)) { if (isNaN(unixTimestamp)) {
return 'Invalid time'; return "Invalid time";
} }
try { try {
@ -125,7 +156,7 @@ export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: UiC
const options: Intl.DateTimeFormatOptions = { const options: Intl.DateTimeFormatOptions = {
dateStyle: date_style, dateStyle: date_style,
timeStyle: time_style, timeStyle: time_style,
hour12: time_format !== 'browser' ? time_format == '12hour' : undefined, hour12: time_format !== "browser" ? time_format == "12hour" : undefined,
}; };
// Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config // Only set timeZone option when resolvedTimeZone does not match UTC±HH:MM format, or when timezone is set in config
@ -143,15 +174,26 @@ export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: UiC
// fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat // fallback if the browser does not support dateStyle/timeStyle in Intl.DateTimeFormat
// This works even tough the timezone is undefined, it will use the runtime's default time zone // This works even tough the timezone is undefined, it will use the runtime's default time zone
if (!containsTime) { if (!containsTime) {
const dateOptions = { ...formatMap[date_style ?? ""]?.date, timeZone: options.timeZone, hour12: options.hour12 }; const dateOptions = {
const timeOptions = { ...formatMap[time_style ?? ""]?.time, timeZone: options.timeZone, hour12: options.hour12 }; ...formatMap[date_style ?? ""]?.date,
timeZone: options.timeZone,
hour12: options.hour12,
};
const timeOptions = {
...formatMap[time_style ?? ""]?.time,
timeZone: options.timeZone,
hour12: options.hour12,
};
return `${date.toLocaleDateString(locale, dateOptions)} ${date.toLocaleTimeString(locale, timeOptions)}`; return `${date.toLocaleDateString(
locale,
dateOptions
)} ${date.toLocaleTimeString(locale, timeOptions)}`;
} }
return formattedDateTime; return formattedDateTime;
} catch (error) { } catch (error) {
return 'Invalid time'; return "Invalid time";
} }
}; };
@ -169,28 +211,31 @@ interface DurationToken {
* @param end_time: number|null - Unix timestamp for end time * @param end_time: number|null - Unix timestamp for end time
* @returns string - duration or 'In Progress' if end time is not provided * @returns string - duration or 'In Progress' if end time is not provided
*/ */
export const getDurationFromTimestamps = (start_time: number, end_time: number | null): string => { export const getDurationFromTimestamps = (
start_time: number,
end_time: number | null
): string => {
if (isNaN(start_time)) { if (isNaN(start_time)) {
return 'Invalid start time'; return "Invalid start time";
} }
let duration = 'In Progress'; let duration = "In Progress";
if (end_time !== null) { if (end_time !== null) {
if (isNaN(end_time)) { if (isNaN(end_time)) {
return 'Invalid end time'; return "Invalid end time";
} }
const start = fromUnixTime(start_time); const start = fromUnixTime(start_time);
const end = fromUnixTime(end_time); const end = fromUnixTime(end_time);
const formatDistanceLocale: DurationToken = { const formatDistanceLocale: DurationToken = {
xSeconds: '{{count}}s', xSeconds: "{{count}}s",
xMinutes: '{{count}}m', xMinutes: "{{count}}m",
xHours: '{{count}}h', xHours: "{{count}}h",
}; };
const shortEnLocale = { const shortEnLocale = {
formatDistance: (token: keyof DurationToken, count: number) => formatDistance: (token: keyof DurationToken, count: number) =>
formatDistanceLocale[token].replace('{{count}}', count.toString()), formatDistanceLocale[token].replace("{{count}}", count.toString()),
}; };
duration = formatDuration(intervalToDuration({ start, end }), { duration = formatDuration(intervalToDuration({ start, end }), {
format: ['hours', 'minutes', 'seconds'], format: ["hours", "minutes", "seconds"],
locale: shortEnLocale, locale: shortEnLocale,
}); });
} }
@ -209,14 +254,18 @@ const getUTCOffset = (date: Date, timezone: string): number => {
if (utcOffsetMatch) { if (utcOffsetMatch) {
const hours = parseInt(utcOffsetMatch[2], 10); const hours = parseInt(utcOffsetMatch[2], 10);
const minutes = parseInt(utcOffsetMatch[3], 10); const minutes = parseInt(utcOffsetMatch[3], 10);
return (utcOffsetMatch[1] === '+' ? 1 : -1) * (hours * 60 + minutes); return (utcOffsetMatch[1] === "+" ? 1 : -1) * (hours * 60 + minutes);
} }
// Otherwise, calculate offset using provided timezone // Otherwise, calculate offset using provided timezone
const utcDate = new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000); const utcDate = new Date(
date.getTime() - date.getTimezoneOffset() * 60 * 1000
);
// locale of en-CA is required for proper locale format // locale of en-CA is required for proper locale format
let iso = utcDate.toLocaleString('en-CA', { timeZone: timezone, hour12: false }).replace(', ', 'T'); let iso = utcDate
iso += `.${utcDate.getMilliseconds().toString().padStart(3, '0')}`; .toLocaleString("en-CA", { timeZone: timezone, hour12: false })
.replace(", ", "T");
iso += `.${utcDate.getMilliseconds().toString().padStart(3, "0")}`;
let target = new Date(`${iso}Z`); let target = new Date(`${iso}Z`);
// safari doesn't like the default format // safari doesn't like the default format