mirror of
https://github.com/home-assistant/frontend.git
synced 2026-07-18 17:16:55 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c93661eaf7 | |||
| 7dac2e243f | |||
| f9e1023a2e | |||
| 82a0d9a413 | |||
| 1325acdba3 | |||
| 07878563be | |||
| b0d5aa5e27 | |||
| 444f98df66 | |||
| f5e62a2c7d |
@@ -0,0 +1,310 @@
|
|||||||
|
export const RETRO_THEME = {
|
||||||
|
// Sharp corners
|
||||||
|
"ha-border-radius-sm": "0",
|
||||||
|
"ha-border-radius-md": "0",
|
||||||
|
"ha-border-radius-lg": "0",
|
||||||
|
"ha-border-radius-xl": "0",
|
||||||
|
"ha-border-radius-2xl": "0",
|
||||||
|
"ha-border-radius-3xl": "0",
|
||||||
|
"ha-border-radius-4xl": "0",
|
||||||
|
"ha-border-radius-5xl": "0",
|
||||||
|
"ha-border-radius-6xl": "0",
|
||||||
|
"ha-border-radius-pill": "0",
|
||||||
|
"ha-border-radius-circle": "0",
|
||||||
|
|
||||||
|
// Fonts
|
||||||
|
"ha-font-family-body":
|
||||||
|
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||||
|
"ha-font-family-heading":
|
||||||
|
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||||
|
"ha-font-family-code": "'Courier New', Courier, monospace",
|
||||||
|
"ha-font-family-longform":
|
||||||
|
"Tahoma, 'MS Sans Serif', 'Microsoft Sans Serif', Arial, sans-serif",
|
||||||
|
|
||||||
|
// No transparency
|
||||||
|
"ha-dialog-scrim-backdrop-filter": "none",
|
||||||
|
|
||||||
|
// Disable animations
|
||||||
|
"ha-animation-duration-fast": "1ms",
|
||||||
|
"ha-animation-duration-normal": "1ms",
|
||||||
|
"ha-animation-duration-slow": "1ms",
|
||||||
|
|
||||||
|
modes: {
|
||||||
|
light: {
|
||||||
|
// Base colors
|
||||||
|
"primary-color": "#000080",
|
||||||
|
"dark-primary-color": "#00006B",
|
||||||
|
"light-primary-color": "#4040C0",
|
||||||
|
"accent-color": "#000080",
|
||||||
|
"primary-text-color": "#000000",
|
||||||
|
"secondary-text-color": "#404040",
|
||||||
|
"text-primary-color": "#ffffff",
|
||||||
|
"text-light-primary-color": "#000000",
|
||||||
|
"disabled-text-color": "#808080",
|
||||||
|
|
||||||
|
// Backgrounds
|
||||||
|
"primary-background-color": "#C0C0C0",
|
||||||
|
"lovelace-background": "#008080",
|
||||||
|
"secondary-background-color": "#C0C0C0",
|
||||||
|
"card-background-color": "#C0C0C0",
|
||||||
|
"clear-background-color": "#C0C0C0",
|
||||||
|
|
||||||
|
// RGB values
|
||||||
|
"rgb-primary-color": "0, 0, 128",
|
||||||
|
"rgb-accent-color": "0, 0, 128",
|
||||||
|
"rgb-primary-text-color": "0, 0, 0",
|
||||||
|
"rgb-secondary-text-color": "64, 64, 64",
|
||||||
|
"rgb-text-primary-color": "255, 255, 255",
|
||||||
|
"rgb-card-background-color": "192, 192, 192",
|
||||||
|
|
||||||
|
// UI chrome
|
||||||
|
"divider-color": "#808080",
|
||||||
|
"outline-color": "#808080",
|
||||||
|
"outline-hover-color": "#404040",
|
||||||
|
"shadow-color": "rgba(0, 0, 0, 0.5)",
|
||||||
|
"scrollbar-thumb-color": "#808080",
|
||||||
|
"disabled-color": "#808080",
|
||||||
|
|
||||||
|
// Cards - retro bevel effect
|
||||||
|
"ha-card-border-width": "1px",
|
||||||
|
"ha-card-border-color": "#808080",
|
||||||
|
"ha-card-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||||
|
"ha-card-border-radius": "0",
|
||||||
|
|
||||||
|
// Dialogs
|
||||||
|
"ha-dialog-border-radius": "0",
|
||||||
|
"ha-dialog-surface-background": "#C0C0C0",
|
||||||
|
"dialog-box-shadow": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||||
|
|
||||||
|
// Box shadows - retro bevel
|
||||||
|
"ha-box-shadow-s": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||||
|
"ha-box-shadow-m": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||||
|
"ha-box-shadow-l": "1px 1px 0 #404040, -1px -1px 0 #ffffff",
|
||||||
|
|
||||||
|
// Header
|
||||||
|
"app-header-background-color": "#000080",
|
||||||
|
"app-header-text-color": "#ffffff",
|
||||||
|
"app-header-border-bottom": "2px outset #C0C0C0",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
"sidebar-background-color": "#C0C0C0",
|
||||||
|
"sidebar-text-color": "#000000",
|
||||||
|
"sidebar-selected-text-color": "#ffffff",
|
||||||
|
"sidebar-selected-icon-color": "#000080",
|
||||||
|
"sidebar-icon-color": "#000000",
|
||||||
|
|
||||||
|
// Input
|
||||||
|
"input-fill-color": "#C0C0C0",
|
||||||
|
"input-disabled-fill-color": "#C0C0C0",
|
||||||
|
"input-ink-color": "#000000",
|
||||||
|
"input-label-ink-color": "#000000",
|
||||||
|
"input-disabled-ink-color": "#808080",
|
||||||
|
"input-idle-line-color": "#808080",
|
||||||
|
"input-hover-line-color": "#000000",
|
||||||
|
"input-disabled-line-color": "#808080",
|
||||||
|
"input-outlined-idle-border-color": "#808080",
|
||||||
|
"input-outlined-hover-border-color": "#000000",
|
||||||
|
"input-outlined-disabled-border-color": "#C0C0C0",
|
||||||
|
"input-dropdown-icon-color": "#000000",
|
||||||
|
|
||||||
|
// Status colors
|
||||||
|
"error-color": "#FF0000",
|
||||||
|
"warning-color": "#FF8000",
|
||||||
|
"success-color": "#008000",
|
||||||
|
"info-color": "#000080",
|
||||||
|
|
||||||
|
// State
|
||||||
|
"state-icon-color": "#000080",
|
||||||
|
"state-active-color": "#000080",
|
||||||
|
"state-inactive-color": "#808080",
|
||||||
|
|
||||||
|
// Data table
|
||||||
|
"data-table-border-width": "0",
|
||||||
|
|
||||||
|
// Primary scale
|
||||||
|
"ha-color-primary-05": "#00003A",
|
||||||
|
"ha-color-primary-10": "#000050",
|
||||||
|
"ha-color-primary-20": "#000066",
|
||||||
|
"ha-color-primary-30": "#00007A",
|
||||||
|
"ha-color-primary-40": "#000080",
|
||||||
|
"ha-color-primary-50": "#0000AA",
|
||||||
|
"ha-color-primary-60": "#4040C0",
|
||||||
|
"ha-color-primary-70": "#6060D0",
|
||||||
|
"ha-color-primary-80": "#8080E0",
|
||||||
|
"ha-color-primary-90": "#C8C8D8",
|
||||||
|
"ha-color-primary-95": "#D8D8E0",
|
||||||
|
|
||||||
|
// Neutral scale
|
||||||
|
"ha-color-neutral-05": "#000000",
|
||||||
|
"ha-color-neutral-10": "#2A2A2A",
|
||||||
|
"ha-color-neutral-20": "#404040",
|
||||||
|
"ha-color-neutral-30": "#606060",
|
||||||
|
"ha-color-neutral-40": "#707070",
|
||||||
|
"ha-color-neutral-50": "#808080",
|
||||||
|
"ha-color-neutral-60": "#909090",
|
||||||
|
"ha-color-neutral-70": "#A0A0A0",
|
||||||
|
"ha-color-neutral-80": "#B0B0B0",
|
||||||
|
"ha-color-neutral-90": "#C8C8C8",
|
||||||
|
"ha-color-neutral-95": "#D0D0D0",
|
||||||
|
|
||||||
|
// Codemirror
|
||||||
|
"codemirror-keyword": "#000080",
|
||||||
|
"codemirror-operator": "#000000",
|
||||||
|
"codemirror-variable": "#008080",
|
||||||
|
"codemirror-variable-2": "#000080",
|
||||||
|
"codemirror-variable-3": "#808000",
|
||||||
|
"codemirror-builtin": "#800080",
|
||||||
|
"codemirror-atom": "#008080",
|
||||||
|
"codemirror-number": "#FF0000",
|
||||||
|
"codemirror-def": "#000080",
|
||||||
|
"codemirror-string": "#008000",
|
||||||
|
"codemirror-string-2": "#808000",
|
||||||
|
"codemirror-comment": "#808080",
|
||||||
|
"codemirror-tag": "#800000",
|
||||||
|
"codemirror-meta": "#000080",
|
||||||
|
"codemirror-attribute": "#FF0000",
|
||||||
|
"codemirror-property": "#000080",
|
||||||
|
"codemirror-qualifier": "#808000",
|
||||||
|
"codemirror-type": "#000080",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
// Base colors
|
||||||
|
"primary-color": "#4040C0",
|
||||||
|
"dark-primary-color": "#000080",
|
||||||
|
"light-primary-color": "#6060D0",
|
||||||
|
"accent-color": "#4040C0",
|
||||||
|
"primary-text-color": "#C0C0C0",
|
||||||
|
"secondary-text-color": "#A0A0A0",
|
||||||
|
"text-primary-color": "#ffffff",
|
||||||
|
"text-light-primary-color": "#C0C0C0",
|
||||||
|
"disabled-text-color": "#606060",
|
||||||
|
|
||||||
|
// Backgrounds
|
||||||
|
"primary-background-color": "#2A2A2A",
|
||||||
|
"lovelace-background": "#003030",
|
||||||
|
"secondary-background-color": "#2A2A2A",
|
||||||
|
"card-background-color": "#3A3A3A",
|
||||||
|
"clear-background-color": "#2A2A2A",
|
||||||
|
|
||||||
|
// RGB values
|
||||||
|
"rgb-primary-color": "64, 64, 192",
|
||||||
|
"rgb-accent-color": "64, 64, 192",
|
||||||
|
"rgb-primary-text-color": "192, 192, 192",
|
||||||
|
"rgb-secondary-text-color": "160, 160, 160",
|
||||||
|
"rgb-text-primary-color": "255, 255, 255",
|
||||||
|
"rgb-card-background-color": "58, 58, 58",
|
||||||
|
|
||||||
|
// UI chrome
|
||||||
|
"divider-color": "#606060",
|
||||||
|
"outline-color": "#606060",
|
||||||
|
"outline-hover-color": "#808080",
|
||||||
|
"shadow-color": "rgba(0, 0, 0, 0.7)",
|
||||||
|
"scrollbar-thumb-color": "#606060",
|
||||||
|
"disabled-color": "#606060",
|
||||||
|
|
||||||
|
// Cards - retro bevel effect
|
||||||
|
"ha-card-border-width": "1px",
|
||||||
|
"ha-card-border-color": "#606060",
|
||||||
|
"ha-card-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||||
|
"ha-card-border-radius": "0",
|
||||||
|
|
||||||
|
// Dialogs
|
||||||
|
"ha-dialog-border-radius": "0",
|
||||||
|
"ha-dialog-surface-background": "#3A3A3A",
|
||||||
|
"dialog-box-shadow": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||||
|
|
||||||
|
// Box shadows - retro bevel
|
||||||
|
"ha-box-shadow-s": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||||
|
"ha-box-shadow-m": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||||
|
"ha-box-shadow-l": "1px 1px 0 #1A1A1A, -1px -1px 0 #5A5A5A",
|
||||||
|
|
||||||
|
// Header
|
||||||
|
"app-header-background-color": "#000060",
|
||||||
|
"app-header-text-color": "#ffffff",
|
||||||
|
"app-header-border-bottom": "2px outset #3A3A3A",
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
"sidebar-background-color": "#2A2A2A",
|
||||||
|
"sidebar-text-color": "#C0C0C0",
|
||||||
|
"sidebar-selected-text-color": "#ffffff",
|
||||||
|
"sidebar-selected-icon-color": "#4040C0",
|
||||||
|
"sidebar-icon-color": "#A0A0A0",
|
||||||
|
|
||||||
|
// Input
|
||||||
|
"input-fill-color": "#3A3A3A",
|
||||||
|
"input-disabled-fill-color": "#3A3A3A",
|
||||||
|
"input-ink-color": "#C0C0C0",
|
||||||
|
"input-label-ink-color": "#A0A0A0",
|
||||||
|
"input-disabled-ink-color": "#606060",
|
||||||
|
"input-idle-line-color": "#606060",
|
||||||
|
"input-hover-line-color": "#808080",
|
||||||
|
"input-disabled-line-color": "#404040",
|
||||||
|
"input-outlined-idle-border-color": "#606060",
|
||||||
|
"input-outlined-hover-border-color": "#808080",
|
||||||
|
"input-outlined-disabled-border-color": "#404040",
|
||||||
|
"input-dropdown-icon-color": "#A0A0A0",
|
||||||
|
|
||||||
|
// Status colors
|
||||||
|
"error-color": "#FF4040",
|
||||||
|
"warning-color": "#FFA040",
|
||||||
|
"success-color": "#40C040",
|
||||||
|
"info-color": "#4040C0",
|
||||||
|
|
||||||
|
// State
|
||||||
|
"state-icon-color": "#4040C0",
|
||||||
|
"state-active-color": "#4040C0",
|
||||||
|
"state-inactive-color": "#606060",
|
||||||
|
|
||||||
|
// Data table
|
||||||
|
"data-table-border-width": "0",
|
||||||
|
|
||||||
|
// Primary scale
|
||||||
|
"ha-color-primary-05": "#00002A",
|
||||||
|
"ha-color-primary-10": "#000040",
|
||||||
|
"ha-color-primary-20": "#000060",
|
||||||
|
"ha-color-primary-30": "#000080",
|
||||||
|
"ha-color-primary-40": "#4040C0",
|
||||||
|
"ha-color-primary-50": "#6060D0",
|
||||||
|
"ha-color-primary-60": "#8080E0",
|
||||||
|
"ha-color-primary-70": "#A0A0F0",
|
||||||
|
"ha-color-primary-80": "#C0C0FF",
|
||||||
|
"ha-color-primary-90": "#3A3A58",
|
||||||
|
"ha-color-primary-95": "#303048",
|
||||||
|
|
||||||
|
// Neutral scale
|
||||||
|
"ha-color-neutral-05": "#1A1A1A",
|
||||||
|
"ha-color-neutral-10": "#2A2A2A",
|
||||||
|
"ha-color-neutral-20": "#3A3A3A",
|
||||||
|
"ha-color-neutral-30": "#4A4A4A",
|
||||||
|
"ha-color-neutral-40": "#606060",
|
||||||
|
"ha-color-neutral-50": "#707070",
|
||||||
|
"ha-color-neutral-60": "#808080",
|
||||||
|
"ha-color-neutral-70": "#909090",
|
||||||
|
"ha-color-neutral-80": "#A0A0A0",
|
||||||
|
"ha-color-neutral-90": "#C0C0C0",
|
||||||
|
"ha-color-neutral-95": "#D0D0D0",
|
||||||
|
|
||||||
|
// Codemirror
|
||||||
|
"codemirror-keyword": "#8080E0",
|
||||||
|
"codemirror-operator": "#C0C0C0",
|
||||||
|
"codemirror-variable": "#40C0C0",
|
||||||
|
"codemirror-variable-2": "#8080E0",
|
||||||
|
"codemirror-variable-3": "#C0C040",
|
||||||
|
"codemirror-builtin": "#C040C0",
|
||||||
|
"codemirror-atom": "#40C0C0",
|
||||||
|
"codemirror-number": "#FF6060",
|
||||||
|
"codemirror-def": "#8080E0",
|
||||||
|
"codemirror-string": "#40C040",
|
||||||
|
"codemirror-string-2": "#C0C040",
|
||||||
|
"codemirror-comment": "#808080",
|
||||||
|
"codemirror-tag": "#C04040",
|
||||||
|
"codemirror-meta": "#8080E0",
|
||||||
|
"codemirror-attribute": "#FF6060",
|
||||||
|
"codemirror-property": "#8080E0",
|
||||||
|
"codemirror-qualifier": "#C0C040",
|
||||||
|
"codemirror-type": "#8080E0",
|
||||||
|
"map-filter":
|
||||||
|
"invert(0.9) hue-rotate(170deg) brightness(1.5) contrast(1.2) saturate(0.3)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,683 @@
|
|||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import {
|
||||||
|
applyThemesOnElement,
|
||||||
|
invalidateThemeCache,
|
||||||
|
} from "../common/dom/apply_themes_on_element";
|
||||||
|
import type { LocalizeKeys } from "../common/translations/localize";
|
||||||
|
import { subscribeLabFeature } from "../data/labs";
|
||||||
|
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||||
|
import type { HomeAssistant } from "../types";
|
||||||
|
import { RETRO_THEME } from "./ha-retro-theme";
|
||||||
|
|
||||||
|
const TIP_COUNT = 25;
|
||||||
|
|
||||||
|
type CasitaExpression =
|
||||||
|
| "hi"
|
||||||
|
| "ok-nabu"
|
||||||
|
| "heart"
|
||||||
|
| "sleep"
|
||||||
|
| "great-job"
|
||||||
|
| "error";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "retro-position";
|
||||||
|
const DRAG_THRESHOLD = 5;
|
||||||
|
const BUBBLE_TIMEOUT = 8000;
|
||||||
|
const SLEEP_TIMEOUT = 30000;
|
||||||
|
const BSOD_CLICK_COUNT = 5;
|
||||||
|
const BSOD_CLICK_TIMEOUT = 3000;
|
||||||
|
const BSOD_DISMISS_DELAY = 500;
|
||||||
|
|
||||||
|
@customElement("ha-retro")
|
||||||
|
export class HaRetro extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@state() private _enabled = false;
|
||||||
|
|
||||||
|
public hassSubscribe() {
|
||||||
|
return [
|
||||||
|
subscribeLabFeature(
|
||||||
|
this.hass!.connection,
|
||||||
|
"frontend",
|
||||||
|
"retro",
|
||||||
|
(feature) => {
|
||||||
|
this._enabled = feature.enabled;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@state() private _casitaVisible = true;
|
||||||
|
|
||||||
|
@state() private _showBubble = false;
|
||||||
|
|
||||||
|
@state() private _bubbleText = "";
|
||||||
|
|
||||||
|
@state() private _expression: CasitaExpression = "hi";
|
||||||
|
|
||||||
|
@state() private _position: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
|
@state() private _showBsod = false;
|
||||||
|
|
||||||
|
private _clickCount = 0;
|
||||||
|
|
||||||
|
private _clickTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
private _dragging = false;
|
||||||
|
|
||||||
|
private _dragStartX = 0;
|
||||||
|
|
||||||
|
private _dragStartY = 0;
|
||||||
|
|
||||||
|
private _dragOffsetX = 0;
|
||||||
|
|
||||||
|
private _dragOffsetY = 0;
|
||||||
|
|
||||||
|
private _dragMoved = false;
|
||||||
|
|
||||||
|
private _bubbleTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
private _sleepTimer?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
private _boundPointerMove = this._onPointerMove.bind(this);
|
||||||
|
|
||||||
|
private _boundPointerUp = this._onPointerUp.bind(this);
|
||||||
|
|
||||||
|
private _themeApplied = false;
|
||||||
|
|
||||||
|
private _isApplyingTheme = false;
|
||||||
|
|
||||||
|
private _themeObserver?: MutationObserver;
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
super.connectedCallback();
|
||||||
|
this._loadPosition();
|
||||||
|
this._resetSleepTimer();
|
||||||
|
this._applyRetroTheme();
|
||||||
|
this._startThemeObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
super.disconnectedCallback();
|
||||||
|
this._clearTimers();
|
||||||
|
this._stopThemeObserver();
|
||||||
|
this._revertTheme();
|
||||||
|
document.removeEventListener("pointermove", this._boundPointerMove);
|
||||||
|
document.removeEventListener("pointerup", this._boundPointerUp);
|
||||||
|
document.removeEventListener("keydown", this._boundDismissBsod);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: Map<string, unknown>): void {
|
||||||
|
if (changedProps.has("_enabled")) {
|
||||||
|
if (this._enabled) {
|
||||||
|
this.hass!.loadFragmentTranslation("retro");
|
||||||
|
this._applyRetroTheme();
|
||||||
|
this._startThemeObserver();
|
||||||
|
} else {
|
||||||
|
this._stopThemeObserver();
|
||||||
|
this._revertTheme();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changedProps.has("hass") && this._enabled) {
|
||||||
|
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||||
|
// Re-apply if darkMode changed
|
||||||
|
if (oldHass && oldHass.themes.darkMode !== this.hass!.themes.darkMode) {
|
||||||
|
this._themeApplied = false;
|
||||||
|
this._applyRetroTheme();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _startThemeObserver(): void {
|
||||||
|
if (this._themeObserver) return;
|
||||||
|
this._themeObserver = new MutationObserver(() => {
|
||||||
|
if (this._isApplyingTheme || !this._enabled || !this.hass) return;
|
||||||
|
// Check if our theme was overwritten by the themes mixin
|
||||||
|
const el = document.documentElement as HTMLElement & {
|
||||||
|
__themes?: { cacheKey?: string };
|
||||||
|
};
|
||||||
|
if (!el.__themes?.cacheKey?.startsWith("Retro")) {
|
||||||
|
this._themeApplied = false;
|
||||||
|
this._applyRetroTheme();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._themeObserver.observe(document.documentElement, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ["style"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopThemeObserver(): void {
|
||||||
|
this._themeObserver?.disconnect();
|
||||||
|
this._themeObserver = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _applyRetroTheme(): void {
|
||||||
|
if (!this.hass || this._themeApplied) return;
|
||||||
|
|
||||||
|
this._isApplyingTheme = true;
|
||||||
|
|
||||||
|
const themes = {
|
||||||
|
...this.hass.themes,
|
||||||
|
themes: {
|
||||||
|
...this.hass.themes.themes,
|
||||||
|
Retro: RETRO_THEME,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
invalidateThemeCache();
|
||||||
|
applyThemesOnElement(
|
||||||
|
document.documentElement,
|
||||||
|
themes,
|
||||||
|
"Retro",
|
||||||
|
{ dark: this.hass.themes.darkMode },
|
||||||
|
true
|
||||||
|
);
|
||||||
|
this._themeApplied = true;
|
||||||
|
this._isApplyingTheme = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _revertTheme(): void {
|
||||||
|
if (!this.hass || !this._themeApplied) return;
|
||||||
|
|
||||||
|
this._isApplyingTheme = true;
|
||||||
|
|
||||||
|
invalidateThemeCache();
|
||||||
|
applyThemesOnElement(
|
||||||
|
document.documentElement,
|
||||||
|
this.hass.themes,
|
||||||
|
this.hass.selectedTheme?.theme || "default",
|
||||||
|
{
|
||||||
|
dark: this.hass.themes.darkMode,
|
||||||
|
primaryColor: this.hass.selectedTheme?.primaryColor,
|
||||||
|
accentColor: this.hass.selectedTheme?.accentColor,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
);
|
||||||
|
this._themeApplied = false;
|
||||||
|
this._isApplyingTheme = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _loadPosition(): void {
|
||||||
|
try {
|
||||||
|
const stored = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (stored) {
|
||||||
|
const pos = JSON.parse(stored);
|
||||||
|
if (typeof pos.x === "number" && typeof pos.y === "number") {
|
||||||
|
this._position = this._clampPosition(pos.x, pos.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore invalid stored position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _savePosition(): void {
|
||||||
|
if (this._position) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(this._position));
|
||||||
|
} catch {
|
||||||
|
// Ignore storage errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clampPosition(x: number, y: number): { x: number; y: number } {
|
||||||
|
const size = 80;
|
||||||
|
return {
|
||||||
|
x: Math.max(0, Math.min(window.innerWidth - size, x)),
|
||||||
|
y: Math.max(0, Math.min(window.innerHeight - size, y)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onPointerDown(ev: PointerEvent): void {
|
||||||
|
if (ev.button !== 0 || this._showBsod) return;
|
||||||
|
|
||||||
|
this._dragging = true;
|
||||||
|
this._dragMoved = false;
|
||||||
|
this._dragStartX = ev.clientX;
|
||||||
|
this._dragStartY = ev.clientY;
|
||||||
|
|
||||||
|
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
this._dragOffsetX = ev.clientX - rect.left;
|
||||||
|
this._dragOffsetY = ev.clientY - rect.top;
|
||||||
|
|
||||||
|
(ev.currentTarget as HTMLElement).setPointerCapture(ev.pointerId);
|
||||||
|
document.addEventListener("pointermove", this._boundPointerMove);
|
||||||
|
document.addEventListener("pointerup", this._boundPointerUp);
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onPointerMove(ev: PointerEvent): void {
|
||||||
|
if (!this._dragging) return;
|
||||||
|
|
||||||
|
const dx = ev.clientX - this._dragStartX;
|
||||||
|
const dy = ev.clientY - this._dragStartY;
|
||||||
|
|
||||||
|
if (!this._dragMoved && Math.hypot(dx, dy) < DRAG_THRESHOLD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._dragMoved = true;
|
||||||
|
|
||||||
|
const x = ev.clientX - this._dragOffsetX;
|
||||||
|
const y = ev.clientY - this._dragOffsetY;
|
||||||
|
this._position = this._clampPosition(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _onPointerUp(ev: PointerEvent): void {
|
||||||
|
document.removeEventListener("pointermove", this._boundPointerMove);
|
||||||
|
document.removeEventListener("pointerup", this._boundPointerUp);
|
||||||
|
|
||||||
|
this._dragging = false;
|
||||||
|
|
||||||
|
if (this._dragMoved) {
|
||||||
|
this._savePosition();
|
||||||
|
} else {
|
||||||
|
this._toggleBubble();
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _stopPropagation(ev: Event): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dismiss(ev: Event): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._casitaVisible = false;
|
||||||
|
this._clearTimers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleBubble(): void {
|
||||||
|
this._clickCount++;
|
||||||
|
if (this._clickTimer) {
|
||||||
|
clearTimeout(this._clickTimer);
|
||||||
|
}
|
||||||
|
this._clickTimer = setTimeout(() => {
|
||||||
|
this._clickCount = 0;
|
||||||
|
}, BSOD_CLICK_TIMEOUT);
|
||||||
|
|
||||||
|
if (this._clickCount >= BSOD_CLICK_COUNT) {
|
||||||
|
this._clickCount = 0;
|
||||||
|
this._triggerBsod();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._showBubble) {
|
||||||
|
this._hideBubble();
|
||||||
|
} else {
|
||||||
|
this._showTip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _boundDismissBsod = this._dismissBsodOnKey.bind(this);
|
||||||
|
|
||||||
|
private _bsodReadyToDismiss = false;
|
||||||
|
|
||||||
|
private _triggerBsod(): void {
|
||||||
|
this._hideBubble();
|
||||||
|
this._showBsod = true;
|
||||||
|
this._bsodReadyToDismiss = false;
|
||||||
|
this._expression = "error";
|
||||||
|
// Delay enabling dismiss so the rapid clicks that triggered the BSOD don't immediately close it
|
||||||
|
setTimeout(() => {
|
||||||
|
this._bsodReadyToDismiss = true;
|
||||||
|
document.addEventListener("keydown", this._boundDismissBsod);
|
||||||
|
}, BSOD_DISMISS_DELAY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dismissBsod(): void {
|
||||||
|
if (!this._bsodReadyToDismiss) return;
|
||||||
|
this._showBsod = false;
|
||||||
|
this._expression = "hi";
|
||||||
|
this._resetSleepTimer();
|
||||||
|
document.removeEventListener("keydown", this._boundDismissBsod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _dismissBsodOnKey(): void {
|
||||||
|
this._dismissBsod();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showTip(): void {
|
||||||
|
const tipIndex = Math.floor(Math.random() * TIP_COUNT) + 1;
|
||||||
|
this._bubbleText = this.hass!.localize(
|
||||||
|
`ui.panel.retro.tip_${tipIndex}` as LocalizeKeys
|
||||||
|
);
|
||||||
|
this._showBubble = true;
|
||||||
|
this._expression = "ok-nabu";
|
||||||
|
this._resetSleepTimer();
|
||||||
|
|
||||||
|
if (this._bubbleTimer) {
|
||||||
|
clearTimeout(this._bubbleTimer);
|
||||||
|
}
|
||||||
|
this._bubbleTimer = setTimeout(() => {
|
||||||
|
this._hideBubble();
|
||||||
|
}, BUBBLE_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _hideBubble(): void {
|
||||||
|
this._showBubble = false;
|
||||||
|
this._expression = "hi";
|
||||||
|
this._resetSleepTimer();
|
||||||
|
|
||||||
|
if (this._bubbleTimer) {
|
||||||
|
clearTimeout(this._bubbleTimer);
|
||||||
|
this._bubbleTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _closeBubble(ev: Event): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._hideBubble();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _resetSleepTimer(): void {
|
||||||
|
if (this._sleepTimer) {
|
||||||
|
clearTimeout(this._sleepTimer);
|
||||||
|
}
|
||||||
|
this._sleepTimer = setTimeout(() => {
|
||||||
|
if (!this._showBubble) {
|
||||||
|
this._expression = "sleep";
|
||||||
|
}
|
||||||
|
}, SLEEP_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearTimers(): void {
|
||||||
|
if (this._bubbleTimer) {
|
||||||
|
clearTimeout(this._bubbleTimer);
|
||||||
|
this._bubbleTimer = undefined;
|
||||||
|
}
|
||||||
|
if (this._sleepTimer) {
|
||||||
|
clearTimeout(this._sleepTimer);
|
||||||
|
this._sleepTimer = undefined;
|
||||||
|
}
|
||||||
|
if (this._clickTimer) {
|
||||||
|
clearTimeout(this._clickTimer);
|
||||||
|
this._clickTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._enabled || !this._casitaVisible) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = 80;
|
||||||
|
const posStyle = this._position
|
||||||
|
? `left: ${this._position.x}px; top: ${this._position.y}px;`
|
||||||
|
: `right: 16px; bottom: 16px;`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this._showBsod
|
||||||
|
? html`
|
||||||
|
<div class="bsod" @click=${this._dismissBsod}>
|
||||||
|
<div class="bsod-content">
|
||||||
|
<h1 class="bsod-title">
|
||||||
|
${this.hass!.localize("ui.panel.retro.bsod_title")}
|
||||||
|
</h1>
|
||||||
|
<p>${this.hass!.localize("ui.panel.retro.bsod_error")}</p>
|
||||||
|
<p>
|
||||||
|
* ${this.hass!.localize("ui.panel.retro.bsod_line_1")}<br />
|
||||||
|
* ${this.hass!.localize("ui.panel.retro.bsod_line_2")}
|
||||||
|
</p>
|
||||||
|
<p class="bsod-prompt">
|
||||||
|
${this.hass!.localize("ui.panel.retro.bsod_continue")}
|
||||||
|
<span class="bsod-cursor">_</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<div
|
||||||
|
class="casita-container ${this._dragging ? "dragging" : ""}"
|
||||||
|
style="width: ${size}px; ${posStyle}"
|
||||||
|
aria-hidden="true"
|
||||||
|
@pointerdown=${this._onPointerDown}
|
||||||
|
>
|
||||||
|
${this._showBubble
|
||||||
|
? html`
|
||||||
|
<div class="speech-bubble">
|
||||||
|
<span class="bubble-text">${this._bubbleText}</span>
|
||||||
|
<button
|
||||||
|
class="bubble-close"
|
||||||
|
@pointerdown=${this._stopPropagation}
|
||||||
|
@click=${this._closeBubble}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="bubble-dismiss"
|
||||||
|
@pointerdown=${this._stopPropagation}
|
||||||
|
@click=${this._dismiss}
|
||||||
|
>
|
||||||
|
${this.hass!.localize("ui.panel.retro.dismiss")}
|
||||||
|
</button>
|
||||||
|
<div class="bubble-arrow"></div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<img
|
||||||
|
class="casita-image"
|
||||||
|
src="/static/images/voice-assistant/${this._expression}.png"
|
||||||
|
alt="Casita"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static readonly styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.casita-container {
|
||||||
|
position: fixed;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: grab;
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
|
||||||
|
}
|
||||||
|
|
||||||
|
.casita-container.dragging {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.casita-image {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
animation: bob 3s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dragging .casita-image {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.speech-bubble {
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
right: 0;
|
||||||
|
background: #ffffe1;
|
||||||
|
color: #000000;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid #000000;
|
||||||
|
padding: 12px 28px 12px 12px;
|
||||||
|
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
width: 300px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: bubble-in 200ms ease-out;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
right: 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #000000;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-close:hover {
|
||||||
|
background: #e0e0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-dismiss {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #808080;
|
||||||
|
font-family: Tahoma, "MS Sans Serif", Arial, sans-serif;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-dismiss:hover {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-arrow {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -8px;
|
||||||
|
right: 32px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 8px solid transparent;
|
||||||
|
border-right: 8px solid transparent;
|
||||||
|
border-top: 8px solid #ffffe1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bob {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bubble-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsod {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #0000aa;
|
||||||
|
color: #ffffff;
|
||||||
|
font-family: "Lucida Console", "Courier New", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.6;
|
||||||
|
z-index: 10000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: bsod-in 100ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsod-content {
|
||||||
|
max-width: 700px;
|
||||||
|
padding: 32px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsod-title {
|
||||||
|
display: inline-block;
|
||||||
|
background: #aaaaaa;
|
||||||
|
color: #0000aa;
|
||||||
|
padding: 2px 12px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsod-content p {
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsod-prompt {
|
||||||
|
margin-top: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bsod-cursor {
|
||||||
|
animation: blink 1s step-end infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bsod-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
50% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.casita-image {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.speech-bubble {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.bsod {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
.bsod-cursor {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-retro": HaRetro;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
import type { Connection } from "home-assistant-js-websocket";
|
import type { Connection } from "home-assistant-js-websocket";
|
||||||
import { createCollection } from "home-assistant-js-websocket";
|
import { createCollection } from "home-assistant-js-websocket";
|
||||||
|
|
||||||
export interface ThemeVars {
|
export type ThemeVars = Record<string, string>;
|
||||||
// Incomplete
|
|
||||||
"primary-color": string;
|
|
||||||
"text-primary-color": string;
|
|
||||||
"accent-color": string;
|
|
||||||
[key: string]: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Theme = ThemeVars & {
|
export type Theme = ThemeVars & {
|
||||||
modes?: {
|
modes?: {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export class HomeAssistantMain extends LitElement {
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
|
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
|
||||||
|
<ha-retro .hass=${this.hass} .narrow=${this.narrow}></ha-retro>
|
||||||
<ha-drawer
|
<ha-drawer
|
||||||
.type=${sidebarNarrow ? "modal" : ""}
|
.type=${sidebarNarrow ? "modal" : ""}
|
||||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||||
@@ -79,6 +80,7 @@ export class HomeAssistantMain extends LitElement {
|
|||||||
protected firstUpdated() {
|
protected firstUpdated() {
|
||||||
import(/* webpackPreload: true */ "../components/ha-sidebar");
|
import(/* webpackPreload: true */ "../components/ha-sidebar");
|
||||||
import("../components/ha-snowflakes");
|
import("../components/ha-snowflakes");
|
||||||
|
import("../components/ha-retro");
|
||||||
|
|
||||||
if (this.hass.auth.external) {
|
if (this.hass.auth.external) {
|
||||||
this._externalSidebar =
|
this._externalSidebar =
|
||||||
|
|||||||
@@ -46,11 +46,14 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
|
|||||||
const featuresToSort = [...features];
|
const featuresToSort = [...features];
|
||||||
|
|
||||||
return featuresToSort.sort((a, b) => {
|
return featuresToSort.sort((a, b) => {
|
||||||
// Place frontend.winter_mode at the bottom
|
// Place frontend fun features at the bottom
|
||||||
if (a.domain === "frontend" && a.preview_feature === "winter_mode")
|
const funFeatures = ["winter_mode", "retro"];
|
||||||
return 1;
|
const aIsFun =
|
||||||
if (b.domain === "frontend" && b.preview_feature === "winter_mode")
|
a.domain === "frontend" && funFeatures.includes(a.preview_feature);
|
||||||
return -1;
|
const bIsFun =
|
||||||
|
b.domain === "frontend" && funFeatures.includes(b.preview_feature);
|
||||||
|
if (aIsFun && !bIsFun) return 1;
|
||||||
|
if (bIsFun && !aIsFun) return -1;
|
||||||
|
|
||||||
// Sort everything else alphabetically
|
// Sort everything else alphabetically
|
||||||
return domainToName(localize, a.domain).localeCompare(
|
return domainToName(localize, a.domain).localeCompare(
|
||||||
|
|||||||
@@ -10705,6 +10705,39 @@
|
|||||||
"add_card": "Add current view as card",
|
"add_card": "Add current view as card",
|
||||||
"add_card_error": "Unable to add card",
|
"add_card_error": "Unable to add card",
|
||||||
"error_no_data": "You need to select some data sources first."
|
"error_no_data": "You need to select some data sources first."
|
||||||
|
},
|
||||||
|
"retro": {
|
||||||
|
"tip_1": "Try turning your house off and on again.",
|
||||||
|
"tip_2": "If your automation doesn't work, just add more YAML.",
|
||||||
|
"tip_3": "Talk to your devices. They won't answer, but it helps.",
|
||||||
|
"tip_4": "The best way to secure your smart home is to go back to candles.",
|
||||||
|
"tip_5": "Rebooting fixes everything. Everything.",
|
||||||
|
"tip_6": "Naming your vacuum 'DJ Roomba' increases cleaning efficiency by 200%.",
|
||||||
|
"tip_7": "Your automations run better when you're not looking.",
|
||||||
|
"tip_8": "Every time you restart Home Assistant, a smart bulb loses its pairing.",
|
||||||
|
"tip_9": "The cloud is just someone else's Raspberry Pi.",
|
||||||
|
"tip_10": "You can automate your coffee machine, but you still have to drink it yourself.",
|
||||||
|
"tip_11": "You can save energy by not having a home.",
|
||||||
|
"tip_12": "Psst... you can drag me anywhere you want!",
|
||||||
|
"tip_13": "Did you know? I never sleep. Well, sometimes I do. Zzz...",
|
||||||
|
"tip_14": "Zigbee, Z-Wave, Wi-Fi, Thread... so many protocols, so little time.",
|
||||||
|
"tip_15": "The sun can trigger your automations. Nature is the best sensor.",
|
||||||
|
"tip_16": "It looks like you're trying to automate your home! Would you like help?",
|
||||||
|
"tip_17": "My previous job was a paperclip. I got promoted.",
|
||||||
|
"tip_18": "I run entirely on YAML and good vibes.",
|
||||||
|
"tip_19": "Somewhere, a smart plug is blinking and nobody knows why.",
|
||||||
|
"tip_20": "Home Assistant runs on a Raspberry Pi. I run on hopes and dreams.",
|
||||||
|
"tip_21": "Behind every great home, there's someone staring at logs at 2am.",
|
||||||
|
"tip_22": "404: Motivation not found. Try again after coffee.",
|
||||||
|
"tip_23": "There are two types of people: those who back up, and those who will.",
|
||||||
|
"tip_24": "My favorite color is #008080. Don't ask me why.",
|
||||||
|
"tip_25": "Automations are just spicy if-then statements.",
|
||||||
|
"dismiss": "Dismiss me",
|
||||||
|
"bsod_title": "Home Assistant",
|
||||||
|
"bsod_error": "A fatal exception 0E has occurred at C0FF:EE15G00D in VXD L1GHT5(01) + 0FF. The current automation will be terminated.",
|
||||||
|
"bsod_line_1": "Don't worry, nothing is actually broken.",
|
||||||
|
"bsod_line_2": "Your automations are still running. Probably.",
|
||||||
|
"bsod_continue": "Press any key or click to continue"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tips": {
|
"tips": {
|
||||||
|
|||||||
Reference in New Issue
Block a user