From 3611e874ca0177445566c475a422a169a7dd29c4 Mon Sep 17 00:00:00 2001 From: Bernt Christian Egeland Date: Wed, 22 Feb 2023 14:54:16 +0100 Subject: [PATCH] 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 --- frigate/config.py | 17 ++++++++++++ web/package-lock.json | 41 +++++++++++++++++++++++++++ web/package.json | 7 +++-- web/src/routes/Events.jsx | 5 +--- web/src/utils/dateUtil.ts | 58 ++++++++++++++++++++++++++++----------- 5 files changed, 105 insertions(+), 23 deletions(-) diff --git a/frigate/config.py b/frigate/config.py index 70d85ff66..562ac4a50 100644 --- a/frigate/config.py +++ b/frigate/config.py @@ -66,12 +66,29 @@ class LiveModeEnum(str, Enum): webrtc = "webrtc" +class DateTimeStyleEnum(str, Enum): + full = "full" + long = "long" + medium = "medium" + short = "short" + + class UIConfig(FrigateBaseModel): live_mode: LiveModeEnum = Field( default=LiveModeEnum.mse, title="Default Live Mode." ) timezone: Optional[str] = Field(title="Override UI timezone.") 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): diff --git a/web/package-lock.json b/web/package-lock.json index 217dc9425..83d6515ec 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@cycjimmy/jsmpeg-player": "^6.0.5", "axios": "^1.2.2", + "copy-to-clipboard": "3.3.3", "date-fns": "^2.29.3", "idb-keyval": "^6.2.0", "immer": "^9.0.16", @@ -19,6 +20,7 @@ "preact-router": "^4.1.0", "react": "npm:@preact/compat@^17.1.2", "react-dom": "npm:@preact/compat@^17.1.2", + "strftime": "^0.10.1", "swr": "^1.3.0", "video.js": "^7.20.3", "videojs-playlist": "^5.0.0", @@ -3369,6 +3371,14 @@ "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": { "version": "3.26.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.0.tgz", @@ -8547,6 +8557,14 @@ "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": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz", @@ -8901,6 +8919,11 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", @@ -12151,6 +12174,14 @@ "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "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": { "version": "3.26.0", "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": { "version": "0.2.8", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.2.8.tgz", @@ -16154,6 +16190,11 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.0.tgz", diff --git a/web/package.json b/web/package.json index bd76cd031..4e5e92e91 100644 --- a/web/package.json +++ b/web/package.json @@ -18,17 +18,18 @@ "date-fns": "^2.29.3", "idb-keyval": "^6.2.0", "immer": "^9.0.16", + "monaco-yaml": "^4.0.2", "preact": "^10.11.3", "preact-async-route": "^2.2.1", "preact-router": "^4.1.0", "react": "npm:@preact/compat@^17.1.2", "react-dom": "npm:@preact/compat@^17.1.2", - "vite-plugin-monaco-editor": "^1.1.0", - "monaco-yaml": "^4.0.2", + "strftime": "^0.10.1", "swr": "^1.3.0", "video.js": "^7.20.3", "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": { "@preact/preset-vite": "^2.5.0", diff --git a/web/src/routes/Events.jsx b/web/src/routes/Events.jsx index 5558c3fe6..e303e1e2e 100644 --- a/web/src/routes/Events.jsx +++ b/web/src/routes/Events.jsx @@ -287,9 +287,6 @@ export default function Events({ path, ...props }) { return ; } - const timezone = config.ui?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone; - const locale = window.navigator?.language || 'en-US'; - return (
Events @@ -508,7 +505,7 @@ export default function Events({ path, ...props }) {
- {formatUnixTimestampToDateTime(event.start_time, locale, timezone)} + {formatUnixTimestampToDateTime(event.start_time, { ...config.ui })}
- diff --git a/web/src/utils/dateUtil.ts b/web/src/utils/dateUtil.ts index 39cc993b4..2891a690a 100644 --- a/web/src/utils/dateUtil.ts +++ b/web/src/utils/dateUtil.ts @@ -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 epochToLong = (date: number): number => date / 1000; export const dateToLong = (date: Date): number => epochToLong(date.getTime()); -import { fromUnixTime, intervalToDuration, formatDuration } from 'date-fns'; const getDateTimeYesterday = (dateTime: Date): Date => { const twentyFourHoursInMilliseconds = 24 * 60 * 60 * 1000; @@ -17,28 +18,53 @@ export const getNowYesterdayInLong = (): number => { }; /** - * This function takes in a unix timestamp, locale, timezone, - * and returns a dateTime string. - * If unixTimestamp is not provided, it returns 'Invalid time' - * @param unixTimestamp: number - * @param locale: string - * @param timezone: string - * @returns string - dateTime or 'Invalid time' if unixTimestamp is not provided + * This function takes in a Unix timestamp, configuration options for date/time display, and an optional strftime format string, + * and returns a formatted date/time string. + * + * If the Unix timestamp is not provided, it returns "Invalid time". + * + * The configuration options determine how the date and time are formatted. + * 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)) { return 'Invalid time'; } try { 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, { - day: '2-digit', - month: '2-digit', - year: 'numeric', - hour: '2-digit', - minute: '2-digit', - second: '2-digit', - timeZone: timezone, + dateStyle: date_style, + timeStyle: time_style, + timeZone: timezone || Intl.DateTimeFormat().resolvedOptions().timeZone, + hour12: use12hour !== null ? use12hour : undefined, }); return formatter.format(date); } catch (error) {