diff --git a/src/common/datetime/absolute_time.ts b/src/common/datetime/absolute_time.ts new file mode 100644 index 0000000000..df67e845df --- /dev/null +++ b/src/common/datetime/absolute_time.ts @@ -0,0 +1,23 @@ +import { isSameDay, isSameYear } from "date-fns"; +import { FrontendLocaleData } from "../../data/translation"; +import { + formatShortDateTime, + formatShortDateTimeWithYear, +} from "./format_date_time"; +import { formatTime } from "./format_time"; + +export const absoluteTime = ( + from: Date, + locale: FrontendLocaleData, + to?: Date +): string => { + const _to = to ?? new Date(); + + if (isSameDay(from, _to)) { + return formatTime(from, locale); + } + if (isSameYear(from, _to)) { + return formatShortDateTime(from, locale); + } + return formatShortDateTimeWithYear(from, locale); +}; diff --git a/src/common/datetime/format_date_time.ts b/src/common/datetime/format_date_time.ts index 0b6e1dcf8c..ba5b73e2e9 100644 --- a/src/common/datetime/format_date_time.ts +++ b/src/common/datetime/format_date_time.ts @@ -24,6 +24,29 @@ const formatDateTimeMem = memoizeOne( ) ); +// Aug 9, 2021, 8:23 AM +export const formatShortDateTimeWithYear = ( + dateObj: Date, + locale: FrontendLocaleData +) => formatShortDateTimeWithYearMem(locale).format(dateObj); + +const formatShortDateTimeWithYearMem = memoizeOne( + (locale: FrontendLocaleData) => + new Intl.DateTimeFormat( + locale.language === "en" && !useAmPm(locale) + ? "en-u-hc-h23" + : locale.language, + { + year: "numeric", + month: "short", + day: "numeric", + hour: useAmPm(locale) ? "numeric" : "2-digit", + minute: "2-digit", + hour12: useAmPm(locale), + } + ) +); + // Aug 9, 8:23 AM export const formatShortDateTime = ( dateObj: Date, diff --git a/src/components/ha-absolute-time.ts b/src/components/ha-absolute-time.ts new file mode 100644 index 0000000000..f782b32317 --- /dev/null +++ b/src/components/ha-absolute-time.ts @@ -0,0 +1,76 @@ +import { addDays, differenceInMilliseconds, startOfDay } from "date-fns"; +import { PropertyValues, ReactiveElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { absoluteTime } from "../common/datetime/absolute_time"; +import type { HomeAssistant } from "../types"; + +const SAFE_MARGIN = 5 * 1000; + +@customElement("ha-absolute-time") +class HaAbsoluteTime extends ReactiveElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) public datetime?: string | Date; + + private _timeout?: number; + + public disconnectedCallback(): void { + super.disconnectedCallback(); + this._clearTimeout(); + } + + public connectedCallback(): void { + super.connectedCallback(); + if (this.datetime) { + this._updateNextDay(); + } + } + + protected createRenderRoot() { + return this; + } + + protected firstUpdated(changedProps: PropertyValues) { + super.firstUpdated(changedProps); + this._updateAbsolute(); + } + + protected update(changedProps: PropertyValues) { + super.update(changedProps); + this._updateAbsolute(); + } + + private _clearTimeout(): void { + if (this._timeout) { + window.clearTimeout(this._timeout); + this._timeout = undefined; + } + } + + private _updateNextDay(): void { + this._clearTimeout(); + + const now = new Date(); + const nextDay = addDays(startOfDay(now), 1); + const ms = differenceInMilliseconds(nextDay, now) + SAFE_MARGIN; + + this._timeout = window.setTimeout(() => { + this._updateNextDay(); + this._updateAbsolute(); + }, ms); + } + + private _updateAbsolute(): void { + if (!this.datetime) { + this.innerHTML = this.hass.localize("ui.components.absolute_time.never"); + } else { + this.innerHTML = absoluteTime(new Date(this.datetime), this.hass.locale); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-absolute-time": HaAbsoluteTime; + } +} diff --git a/src/dialogs/more-info/components/ha-more-info-state-header.ts b/src/dialogs/more-info/components/ha-more-info-state-header.ts index 8ba2ceb8bb..1e7430cb23 100644 --- a/src/dialogs/more-info/components/ha-more-info-state-header.ts +++ b/src/dialogs/more-info/components/ha-more-info-state-header.ts @@ -1,7 +1,9 @@ import { HassEntity } from "home-assistant-js-websocket"; -import { html, LitElement, TemplateResult, css, CSSResultGroup } from "lit"; -import { customElement, property } from "lit/decorators"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; import { computeStateDisplay } from "../../../common/entity/compute_state_display"; +import "../../../components/ha-absolute-time"; +import "../../../components/ha-relative-time"; import { isUnavailableState } from "../../../data/entity"; import { LightEntity } from "../../../data/light"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; @@ -16,6 +18,8 @@ export class HaMoreInfoStateHeader extends LitElement { @property({ attribute: false }) public stateOverride?: string; + @state() private _absoluteTime = false; + private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string { if ( stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP && @@ -41,15 +45,32 @@ export class HaMoreInfoStateHeader extends LitElement { return stateDisplay; } - protected render(): TemplateResult { - const name = this.stateObj.attributes.friendly_name; + private _toggleAbsolute() { + this._absoluteTime = !this._absoluteTime; + } + protected render(): TemplateResult { const stateDisplay = this.stateOverride ?? this._computeStateDisplay(this.stateObj); return html` -

${name}

${stateDisplay}

+

+ ${this._absoluteTime + ? html` + + ` + : html` + + `} +

`; } @@ -59,20 +80,24 @@ export class HaMoreInfoStateHeader extends LitElement { text-align: center; margin: 0; } - .name { + .state { font-style: normal; font-weight: 400; - font-size: 28px; - line-height: 36px; - margin-bottom: 4px; + font-size: 36px; + line-height: 44px; } - .state { + .last-changed { font-style: normal; font-weight: 500; font-size: 16px; line-height: 24px; letter-spacing: 0.1px; - margin-bottom: 24px; + padding: 4px 0; + margin-bottom: 20px; + cursor: pointer; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } `; } diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts index b994cbb874..c66b847a96 100644 --- a/src/dialogs/more-info/ha-more-info-dialog.ts +++ b/src/dialogs/more-info/ha-more-info-dialog.ts @@ -36,7 +36,6 @@ import { HomeAssistant } from "../../types"; import { computeShowHistoryComponent, computeShowLogBookComponent, - computeShowNewMoreInfo, DOMAINS_WITH_MORE_INFO, EDITABLE_DOMAINS_WITH_ID, EDITABLE_DOMAINS_WITH_UNIQUE_ID, @@ -240,7 +239,6 @@ export class MoreInfoDialog extends LitElement { const title = this._childView?.viewTitle ?? name; const isInfoView = this._currView === "info" && !this._childView; - const isNewMoreInfo = stateObj && computeShowNewMoreInfo(stateObj); return html` @@ -265,13 +263,9 @@ export class MoreInfoDialog extends LitElement { )} > `} - ${!isInfoView || !isNewMoreInfo - ? html` - - ${title} - - ` - : nothing} + + ${title} + ${isInfoView ? html` ${this.shouldShowHistory(domain) diff --git a/src/translations/en.json b/src/translations/en.json index df6d3998a5..a6231f37a3 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -509,6 +509,9 @@ "relative_time": { "never": "Never" }, + "absolute_time": { + "never": "[%key:ui::components::relative_time::never%]" + }, "history_charts": { "history_disabled": "History integration disabled", "loading_history": "Loading state history…",