Events Page: Added option to override browser time format and style (#5538)

* use12hour optional config

* use12hour config arg

* added use12HourFormat arg to format function

* dateStyle & timeStyle option

* moved timezone & locales to format function

* added dateStyle & timeStyle

* re-formatted

* added strftime_fmt config entry

* strftime package

* added strftime option

* underscore instead of camelCase

* underscore props instead of camelCase
This commit is contained in:
Bernt Christian Egeland 2023-02-22 14:54:16 +01:00 committed by GitHub
parent fbf29667d4
commit 3611e874ca
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 105 additions and 23 deletions

View File

@ -66,12 +66,29 @@ class LiveModeEnum(str, Enum):
webrtc = "webrtc" webrtc = "webrtc"
class DateTimeStyleEnum(str, Enum):
full = "full"
long = "long"
medium = "medium"
short = "short"
class UIConfig(FrigateBaseModel): class UIConfig(FrigateBaseModel):
live_mode: LiveModeEnum = Field( live_mode: LiveModeEnum = Field(
default=LiveModeEnum.mse, title="Default Live Mode." default=LiveModeEnum.mse, title="Default Live Mode."
) )
timezone: Optional[str] = Field(title="Override UI timezone.") timezone: Optional[str] = Field(title="Override UI timezone.")
use_experimental: bool = Field(default=False, title="Experimental UI") use_experimental: bool = Field(default=False, title="Experimental UI")
use12hour: Optional[bool] = Field(title="Override UI time format.")
date_style: DateTimeStyleEnum = Field(
default=DateTimeStyleEnum.short, title="Override UI dateStyle."
)
time_style: DateTimeStyleEnum = Field(
default=DateTimeStyleEnum.medium, title="Override UI timeStyle."
)
strftime_fmt: Optional[str] = Field(
default=None, title="Override date and time format using strftime syntax."
)
class TelemetryConfig(FrigateBaseModel): class TelemetryConfig(FrigateBaseModel):

41
web/package-lock.json generated
View File

@ -10,6 +10,7 @@
"dependencies": { "dependencies": {
"@cycjimmy/jsmpeg-player": "^6.0.5", "@cycjimmy/jsmpeg-player": "^6.0.5",
"axios": "^1.2.2", "axios": "^1.2.2",
"copy-to-clipboard": "3.3.3",
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"idb-keyval": "^6.2.0", "idb-keyval": "^6.2.0",
"immer": "^9.0.16", "immer": "^9.0.16",
@ -19,6 +20,7 @@
"preact-router": "^4.1.0", "preact-router": "^4.1.0",
"react": "npm:@preact/compat@^17.1.2", "react": "npm:@preact/compat@^17.1.2",
"react-dom": "npm:@preact/compat@^17.1.2", "react-dom": "npm:@preact/compat@^17.1.2",
"strftime": "^0.10.1",
"swr": "^1.3.0", "swr": "^1.3.0",
"video.js": "^7.20.3", "video.js": "^7.20.3",
"videojs-playlist": "^5.0.0", "videojs-playlist": "^5.0.0",
@ -3369,6 +3371,14 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"dependencies": {
"toggle-selection": "^1.0.6"
}
},
"node_modules/core-js": { "node_modules/core-js": {
"version": "3.26.0", "version": "3.26.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
@ -8547,6 +8557,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/strftime": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.1.tgz",
"integrity": "sha512-nVvH6JG8KlXFPC0f8lojLgEsPA18lRpLZ+RrJh/NkQV2tqOgZfbas8gcU8SFgnnqR3rWzZPYu6N2A3xzs/8rQg==",
"engines": {
"node": ">=0.2.0"
}
},
"node_modules/strict-event-emitter": { "node_modules/strict-event-emitter": {
"version": "0.2.8", "version": "0.2.8",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz",
@ -8901,6 +8919,11 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"node_modules/totalist": { "node_modules/totalist": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",
@ -12151,6 +12174,14 @@
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
"dev": true "dev": true
}, },
"copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
"integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
"requires": {
"toggle-selection": "^1.0.6"
}
},
"core-js": { "core-js": {
"version": "3.26.0", "version": "3.26.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz",
@ -15868,6 +15899,11 @@
} }
} }
}, },
"strftime": {
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.1.tgz",
"integrity": "sha512-nVvH6JG8KlXFPC0f8lojLgEsPA18lRpLZ+RrJh/NkQV2tqOgZfbas8gcU8SFgnnqR3rWzZPYu6N2A3xzs/8rQg=="
},
"strict-event-emitter": { "strict-event-emitter": {
"version": "0.2.8", "version": "0.2.8",
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz",
@ -16154,6 +16190,11 @@
"is-number": "^7.0.0" "is-number": "^7.0.0"
} }
}, },
"toggle-selection": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
"integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="
},
"totalist": { "totalist": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz",

View File

@ -18,17 +18,18 @@
"date-fns": "^2.29.3", "date-fns": "^2.29.3",
"idb-keyval": "^6.2.0", "idb-keyval": "^6.2.0",
"immer": "^9.0.16", "immer": "^9.0.16",
"monaco-yaml": "^4.0.2",
"preact": "^10.11.3", "preact": "^10.11.3",
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^4.1.0", "preact-router": "^4.1.0",
"react": "npm:@preact/compat@^17.1.2", "react": "npm:@preact/compat@^17.1.2",
"react-dom": "npm:@preact/compat@^17.1.2", "react-dom": "npm:@preact/compat@^17.1.2",
"vite-plugin-monaco-editor": "^1.1.0", "strftime": "^0.10.1",
"monaco-yaml": "^4.0.2",
"swr": "^1.3.0", "swr": "^1.3.0",
"video.js": "^7.20.3", "video.js": "^7.20.3",
"videojs-playlist": "^5.0.0", "videojs-playlist": "^5.0.0",
"videojs-seek-buttons": "^3.0.1" "videojs-seek-buttons": "^3.0.1",
"vite-plugin-monaco-editor": "^1.1.0"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.5.0", "@preact/preset-vite": "^2.5.0",

View File

@ -287,9 +287,6 @@ export default function Events({ path, ...props }) {
return <ActivityIndicator />; return <ActivityIndicator />;
} }
const timezone = config.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone;
const locale = window.navigator?.language || 'en-US';
return ( return (
<div className="space-y-4 p-2 px-4 w-full"> <div className="space-y-4 p-2 px-4 w-full">
<Heading>Events</Heading> <Heading>Events</Heading>
@ -508,7 +505,7 @@ export default function Events({ path, ...props }) {
</div> </div>
<div className="text-sm flex"> <div className="text-sm flex">
<Clock className="h-5 w-5 mr-2 inline" /> <Clock className="h-5 w-5 mr-2 inline" />
{formatUnixTimestampToDateTime(event.start_time, locale, timezone)} {formatUnixTimestampToDateTime(event.start_time, { ...config.ui })}
<div className="hidden md:inline"> <div className="hidden md:inline">
<span className="m-1">-</span> <span className="m-1">-</span>
<TimeAgo time={event.start_time * 1000} dense /> <TimeAgo time={event.start_time * 1000} dense />

View File

@ -1,7 +1,8 @@
import strftime from 'strftime';
import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns';
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());
import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns';
const getDateTimeYesterday = (dateTime: Date): Date => { const getDateTimeYesterday = (dateTime: Date): Date => {
const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000;
@ -17,28 +18,53 @@ export const getNowYesterdayInLong = (): number => {
}; };
/** /**
* This function takes in a unix timestamp, locale, timezone, * This function takes in a Unix timestamp, configuration options for date/time display, and an optional strftime format string,
* and returns a dateTime string. * and returns a formatted date/time string.
* If unixTimestamp is not provided, it returns 'Invalid time' *
* @param unixTimestamp: number * If the Unix timestamp is not provided, it returns "Invalid time".
* @param locale: string *
* @param timezone: string * The configuration options determine how the date and time are formatted.
* @returns string - dateTime or 'Invalid time' if unixTimestamp is not provided * The `timezone` option allows you to specify a specific timezone for the output, otherwise the user's browser timezone will be used.
* The `use12hour` option allows you to display time in a 12-hour format if true, and 24-hour format if false.
* The `dateStyle` and `timeStyle` options allow you to specify pre-defined formats for displaying the date and time.
* The `strftime_fmt` option allows you to specify a custom format using the strftime syntax.
*
* If both `strftime_fmt` and `dateStyle`/`timeStyle` are provided, `strftime_fmt` takes precedence.
*
* @param unixTimestamp The Unix timestamp to format
* @param config An object containing the configuration options for date/time display
* @returns The formatted date/time string, or "Invalid time" if the Unix timestamp is not provided or invalid.
*/ */
export const formatUnixTimestampToDateTime = (unixTimestamp: number, locale: string, timezone: string): string => { interface DateTimeStyle {
timezone: string;
use12hour: boolean | undefined;
date_style: 'full' | 'long' | 'medium' | 'short';
time_style: 'full' | 'long' | 'medium' | 'short';
strftime_fmt: string;
}
export const formatUnixTimestampToDateTime = (unixTimestamp: number, config: DateTimeStyle): string => {
const { timezone, use12hour, 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 {
const date = new Date(unixTimestamp * 1000); const date = new Date(unixTimestamp * 1000);
// use strftime_fmt if defined in config file
if (strftime_fmt) {
const strftime_locale = strftime.localizeByIdentifier(locale);
return strftime_locale(strftime_fmt, date);
}
// else use Intl.DateTimeFormat
const formatter = new Intl.DateTimeFormat(locale, { const formatter = new Intl.DateTimeFormat(locale, {
day: '2-digit', dateStyle: date_style,
month: '2-digit', timeStyle: time_style,
year: 'numeric', timeZone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
hour: '2-digit', hour12: use12hour !== null ? use12hour : undefined,
minute: '2-digit',
second: '2-digit',
timeZone: timezone,
}); });
return formatter.format(date); return formatter.format(date);
} catch (error) { } catch (error) {