mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add simple clock card (#24599)
* Initial clock card * Tidy clock card and change stub config * Change fallback to 'nothing' * Update src/panels/lovelace/cards/types.ts Co-authored-by: Paul Bottein <paul.bottein@gmail.com> * Update src/panels/lovelace/cards/hui-clock-card.ts Co-authored-by: Paul Bottein <paul.bottein@gmail.com> * Update src/panels/lovelace/cards/hui-clock-card.ts Co-authored-by: Paul Bottein <paul.bottein@gmail.com> * Added cardSize and gridOptions. Fixed invalid time type * Improve font sizes * Fix default case handling * Move interval outside class * WIP improvements * Various improvements * Improve date instantiation and display * Reintroduce localized time format * Swap to uusing key for time_format translation * Add fallback for initial load * Final fixes * Update clock card description * Update src/panels/lovelace/cards/types.ts Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Tidy up * Present css better * Change default sizing to small * Set default data * Change to grid, rework typography alignment * Update hui-clock-card.ts * Update hui-clock-card.ts --------- Co-authored-by: Paul Bottein <paul.bottein@gmail.com> Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
53bb8251fa
commit
05e303d771
261
src/panels/lovelace/cards/hui-clock-card.ts
Normal file
261
src/panels/lovelace/cards/hui-clock-card.ts
Normal file
@ -0,0 +1,261 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-card";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type {
|
||||
LovelaceCard,
|
||||
LovelaceCardEditor,
|
||||
LovelaceGridOptions,
|
||||
} from "../types";
|
||||
import type { ClockCardConfig } from "./types";
|
||||
import { useAmPm } from "../../../common/datetime/use_am_pm";
|
||||
import { resolveTimeZone } from "../../../common/datetime/resolve-time-zone";
|
||||
|
||||
const INTERVAL = 1000;
|
||||
|
||||
@customElement("hui-clock-card")
|
||||
export class HuiClockCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
await import("../editor/config-elements/hui-clock-card-editor");
|
||||
return document.createElement("hui-clock-card-editor");
|
||||
}
|
||||
|
||||
public static getStubConfig(): ClockCardConfig {
|
||||
return {
|
||||
type: "clock",
|
||||
};
|
||||
}
|
||||
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: ClockCardConfig;
|
||||
|
||||
@state() private _dateTimeFormat?: Intl.DateTimeFormat;
|
||||
|
||||
@state() private _timeHour?: string;
|
||||
|
||||
@state() private _timeMinute?: string;
|
||||
|
||||
@state() private _timeSecond?: string;
|
||||
|
||||
@state() private _timeAmPm?: string;
|
||||
|
||||
private _tickInterval?: undefined | number;
|
||||
|
||||
public setConfig(config: ClockCardConfig): void {
|
||||
this._config = config;
|
||||
this._initDate();
|
||||
}
|
||||
|
||||
private _initDate() {
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
let locale = this.hass?.locale;
|
||||
|
||||
if (this._config?.time_format) {
|
||||
locale = { ...locale, time_format: this._config.time_format };
|
||||
}
|
||||
|
||||
this._dateTimeFormat = new Intl.DateTimeFormat(this.hass.locale.language, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: resolveTimeZone(locale.time_zone, this.hass.config?.time_zone),
|
||||
});
|
||||
|
||||
this._tick();
|
||||
}
|
||||
|
||||
public getCardSize(): number {
|
||||
if (this._config?.clock_size === "small") return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
public getGridOptions(): LovelaceGridOptions {
|
||||
if (this._config?.clock_size === "medium") {
|
||||
return {
|
||||
min_rows: 1,
|
||||
rows: 2,
|
||||
max_rows: 4,
|
||||
min_columns: 4,
|
||||
columns: 6,
|
||||
};
|
||||
}
|
||||
|
||||
if (this._config?.clock_size === "large") {
|
||||
return {
|
||||
min_rows: 2,
|
||||
rows: 2,
|
||||
max_rows: 4,
|
||||
min_columns: 6,
|
||||
columns: 6,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
min_rows: 1,
|
||||
rows: 1,
|
||||
max_rows: 4,
|
||||
min_columns: 4,
|
||||
columns: 6,
|
||||
};
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass");
|
||||
if (!oldHass || oldHass.locale !== this.hass?.locale) {
|
||||
this._initDate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._startTick();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._stopTick();
|
||||
}
|
||||
|
||||
private _startTick() {
|
||||
this._tickInterval = window.setInterval(() => this._tick(), INTERVAL);
|
||||
this._tick();
|
||||
}
|
||||
|
||||
private _stopTick() {
|
||||
if (this._tickInterval) {
|
||||
clearInterval(this._tickInterval);
|
||||
this._tickInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _tick() {
|
||||
if (!this._dateTimeFormat) return;
|
||||
|
||||
const parts = this._dateTimeFormat.formatToParts();
|
||||
|
||||
this._timeHour = parts.find((part) => part.type === "hour")?.value;
|
||||
this._timeMinute = parts.find((part) => part.type === "minute")?.value;
|
||||
this._timeSecond = this._config?.show_seconds
|
||||
? parts.find((part) => part.type === "second")?.value
|
||||
: undefined;
|
||||
this._timeAmPm = parts.find((part) => part.type === "dayPeriod")?.value;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._config) return nothing;
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
<div
|
||||
class="time-wrapper ${this._config.clock_size
|
||||
? `size-${this._config.clock_size}`
|
||||
: ""}"
|
||||
>
|
||||
<div class="time-parts">
|
||||
<div class="time-part hour">${this._timeHour}</div>
|
||||
<div class="time-part minute">${this._timeMinute}</div>
|
||||
${this._timeSecond !== undefined
|
||||
? html`<div class="time-part second">${this._timeSecond}</div>`
|
||||
: nothing}
|
||||
${this._timeAmPm !== undefined
|
||||
? html`<div class="time-part am-pm">${this._timeAmPm}</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.time-wrapper {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time-parts {
|
||||
align-items: center;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"hour minute second"
|
||||
"hour minute am-pm";
|
||||
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
line-height: 0.8;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.time-wrapper.size-medium .time-parts {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.time-wrapper.size-large .time-parts {
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.time-wrapper.size-medium .time-parts .time-part.second,
|
||||
.time-wrapper.size-medium .time-parts .time-part.am-pm {
|
||||
font-size: 16px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.time-wrapper.size-large .time-parts .time-part.second,
|
||||
.time-wrapper.size-large .time-parts .time-part.am-pm {
|
||||
font-size: 24px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.time-parts .time-part.hour {
|
||||
grid-area: hour;
|
||||
}
|
||||
|
||||
.time-parts .time-part.minute {
|
||||
grid-area: minute;
|
||||
}
|
||||
|
||||
.time-parts .time-part.second {
|
||||
grid-area: second;
|
||||
line-height: 0.9;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.time-parts .time-part.am-pm {
|
||||
grid-area: am-pm;
|
||||
line-height: 0.9;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.time-parts .time-part.second,
|
||||
.time-parts .time-part.am-pm {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.time-parts .time-part.hour:after {
|
||||
content: ":";
|
||||
margin: 0 2px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-clock-card": HuiClockCard;
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import type {
|
||||
} from "../entity-rows/types";
|
||||
import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
|
||||
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
|
||||
import type { TimeFormat } from "../../../data/translation";
|
||||
|
||||
export type AlarmPanelCardConfigState =
|
||||
| "arm_away"
|
||||
@ -346,6 +347,13 @@ export interface MarkdownCardConfig extends LovelaceCardConfig {
|
||||
show_empty?: boolean;
|
||||
}
|
||||
|
||||
export interface ClockCardConfig extends LovelaceCardConfig {
|
||||
type: "clock";
|
||||
clock_size?: "small" | "medium" | "large";
|
||||
show_seconds?: boolean | undefined;
|
||||
time_format?: TimeFormat;
|
||||
}
|
||||
|
||||
export interface MediaControlCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
theme?: string;
|
||||
|
@ -76,6 +76,7 @@ const LAZY_LOAD_TYPES = {
|
||||
logbook: () => import("../cards/hui-logbook-card"),
|
||||
map: () => import("../cards/hui-map-card"),
|
||||
markdown: () => import("../cards/hui-markdown-card"),
|
||||
clock: () => import("../cards/hui-clock-card"),
|
||||
"media-control": () => import("../cards/hui-media-control-card"),
|
||||
"picture-elements": () => import("../cards/hui-picture-elements-card"),
|
||||
"picture-entity": () => import("../cards/hui-picture-entity-card"),
|
||||
|
@ -0,0 +1,145 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
assert,
|
||||
assign,
|
||||
boolean,
|
||||
enums,
|
||||
literal,
|
||||
object,
|
||||
optional,
|
||||
union,
|
||||
} from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { ClockCardConfig } from "../../cards/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { TimeFormat } from "../../../../data/translation";
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
object({
|
||||
clock_size: optional(
|
||||
union([literal("small"), literal("medium"), literal("large")])
|
||||
),
|
||||
time_format: optional(enums(Object.values(TimeFormat))),
|
||||
show_seconds: optional(boolean()),
|
||||
})
|
||||
);
|
||||
|
||||
@customElement("hui-clock-card-editor")
|
||||
export class HuiClockCardEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _config?: ClockCardConfig;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "clock_size",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: ["small", "medium", "large"].map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.clock_sizes.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "show_seconds",
|
||||
selector: {
|
||||
boolean: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "time_format",
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
options: Object.values(TimeFormat).map((value) => ({
|
||||
value,
|
||||
label: localize(
|
||||
`ui.panel.lovelace.editor.card.clock.time_formats.${value}`
|
||||
),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
private _data = memoizeOne((config) => ({
|
||||
clock_size: "small",
|
||||
time_format: TimeFormat.language,
|
||||
show_seconds: false,
|
||||
...config,
|
||||
}));
|
||||
|
||||
public setConfig(config: ClockCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._data(this._config)}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "clock_size":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.clock_size`
|
||||
);
|
||||
case "time_format":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.time_format`
|
||||
);
|
||||
case "show_seconds":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.clock.show_seconds`
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-clock-card-editor": HuiClockCardEditor;
|
||||
}
|
||||
}
|
@ -13,6 +13,10 @@ export const coreCards: Card[] = [
|
||||
type: "calendar",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "clock",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "entities",
|
||||
showElement: true,
|
||||
|
@ -7150,6 +7150,24 @@
|
||||
},
|
||||
"description": "The Markdown card is used to render Markdown."
|
||||
},
|
||||
"clock": {
|
||||
"name": "Clock",
|
||||
"description": "The Clock card displays the current time using your desired size and format.",
|
||||
"clock_size": "Clock size",
|
||||
"clock_sizes": {
|
||||
"small": "Small",
|
||||
"medium": "Medium",
|
||||
"large": "Large"
|
||||
},
|
||||
"show_seconds": "Display seconds",
|
||||
"time_format": "Time format",
|
||||
"time_formats": {
|
||||
"language": "[%key:ui::panel::profile::time_format::formats::language%]",
|
||||
"system": "[%key:ui::panel::profile::time_format::formats::system%]",
|
||||
"24": "[%key:ui::panel::profile::time_format::formats::24%]",
|
||||
"12": "[%key:ui::panel::profile::time_format::formats::12%]"
|
||||
}
|
||||
},
|
||||
"media-control": {
|
||||
"name": "Media control",
|
||||
"description": "The Media control card is used to display media player entities on an interface with easy to use controls."
|
||||
|
Loading…
x
Reference in New Issue
Block a user