mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
20230628.0 (#17080)
This commit is contained in:
commit
50f4a1abc5
@ -10,6 +10,12 @@ supports es6-module-dynamic-import
|
||||
not Safari < 13
|
||||
not iOS < 13
|
||||
|
||||
# Exclude KaiOS, QQ, and UC browsers due to lack of sufficient feature support data
|
||||
# Babel ignores these automatically, but we need here for Webpack to output ESM with dynamic imports
|
||||
not KaiOS > 0
|
||||
not QQAndroid > 0
|
||||
not UCAndroid > 0
|
||||
|
||||
# Exclude unsupported browsers
|
||||
not dead
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.10
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.11
|
||||
|
||||
ENV \
|
||||
DEBIAN_FRONTEND=noninteractive \
|
||||
|
5
.github/release-drafter.yml
vendored
5
.github/release-drafter.yml
vendored
@ -1,3 +1,8 @@
|
||||
categories:
|
||||
- title: 'Dependency updates'
|
||||
collapse-after: 3
|
||||
labels:
|
||||
- 'dependencies'
|
||||
template: |
|
||||
## What's Changed
|
||||
|
||||
|
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
@ -47,7 +47,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
@ -65,7 +65,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
@ -83,7 +83,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
with:
|
||||
|
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
|
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@ -58,7 +58,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
|
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v3.6.0
|
||||
|
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@ -9,7 +9,7 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v4.0.0
|
||||
- uses: dessant/lock-threads@v4.0.1
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
issue-lock-inactive-days: "30"
|
||||
|
4
.github/workflows/nightly.yaml
vendored
4
.github/workflows/nightly.yaml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- cron: "0 1 * * *"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.10"
|
||||
PYTHON_VERSION: "3.11"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
permissions:
|
||||
@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@v4
|
||||
|
9
.github/workflows/release-drafter.yaml
vendored
9
.github/workflows/release-drafter.yaml
vendored
@ -5,8 +5,17 @@ on:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
permissions:
|
||||
# write permission for contents is required to create a github release
|
||||
contents: write
|
||||
# write permission for pull-requests is required for autolabeler
|
||||
# otherwise, read permission is required at least
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
|
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@ -6,7 +6,7 @@ on:
|
||||
- published
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.10"
|
||||
PYTHON_VERSION: "3.11"
|
||||
NODE_OPTIONS: --max_old_space_size=6144
|
||||
|
||||
# Set default workflow permissions
|
||||
@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@master
|
||||
@ -76,7 +76,7 @@ jobs:
|
||||
- name: Build wheels
|
||||
uses: home-assistant/wheels@2023.04.0
|
||||
with:
|
||||
abi: cp310
|
||||
abi: cp311
|
||||
tag: musllinux_1_2
|
||||
arch: amd64
|
||||
wheels-key: ${{ secrets.WHEELS_KEY }}
|
||||
|
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@v3.5.2
|
||||
uses: actions/checkout@v3.5.3
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
@ -77,6 +77,7 @@ module.exports.htmlMinifierOptions = {
|
||||
module.exports.terserOptions = ({ latestBuild, isTestBuild }) => ({
|
||||
safari10: !latestBuild,
|
||||
ecma: latestBuild ? 2015 : 5,
|
||||
module: latestBuild,
|
||||
format: { comments: false },
|
||||
sourceMap: !isTestBuild,
|
||||
});
|
||||
@ -97,7 +98,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({
|
||||
"@babel/preset-env",
|
||||
{
|
||||
useBuiltIns: latestBuild ? false : "entry",
|
||||
corejs: latestBuild ? false : { version: "3.30", proposals: true },
|
||||
corejs: latestBuild ? false : { version: "3.31", proposals: true },
|
||||
bugfixes: true,
|
||||
},
|
||||
],
|
||||
|
@ -17,6 +17,7 @@ const modules = {
|
||||
"intl-datetimeformat": "DateTimeFormat",
|
||||
"intl-numberformat": "NumberFormat",
|
||||
"intl-displaynames": "DisplayNames",
|
||||
"intl-listformat": "ListFormat",
|
||||
};
|
||||
|
||||
gulp.task("create-locale-data", (done) => {
|
||||
|
@ -142,4 +142,5 @@ module.exports = {
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createRollupConfig,
|
||||
};
|
||||
|
@ -41,7 +41,7 @@ const createWebpackConfig = ({
|
||||
return {
|
||||
name,
|
||||
mode: isProdBuild ? "production" : "development",
|
||||
target: ["web", latestBuild ? "es2017" : "es5"],
|
||||
target: `browserslist:${latestBuild ? "modern" : "legacy"}`,
|
||||
// For tests/CI, source maps are skipped to gain build speed
|
||||
// For production, generate source maps for accurate stack traces without source code
|
||||
// For development, generate "cheap" versions that can map to original line numbers
|
||||
@ -84,6 +84,13 @@ const createWebpackConfig = ({
|
||||
],
|
||||
moduleIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
chunkIds: isProdBuild && !isStatsBuild ? "deterministic" : "named",
|
||||
splitChunks: {
|
||||
// Disable splitting for web workers with ESM output
|
||||
// Imports of external chunks are broken
|
||||
chunks: latestBuild
|
||||
? (chunk) => !chunk.canBeInitial() && !/^.+-worker$/.test(chunk.name)
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
!isStatsBuild && new WebpackBar({ fancy: !isProdBuild }),
|
||||
@ -160,9 +167,12 @@ const createWebpackConfig = ({
|
||||
"lit/polyfill-support$": "lit/polyfill-support.js",
|
||||
"@lit-labs/virtualizer/layouts/grid":
|
||||
"@lit-labs/virtualizer/layouts/grid.js",
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver":
|
||||
"@lit-labs/virtualizer/polyfills/resize-observer-polyfill/ResizeObserver.js",
|
||||
},
|
||||
},
|
||||
output: {
|
||||
module: latestBuild,
|
||||
filename: ({ chunk }) =>
|
||||
!isProdBuild || isStatsBuild || dontHash.has(chunk.name)
|
||||
? "[name].js"
|
||||
@ -196,7 +206,7 @@ const createWebpackConfig = ({
|
||||
: undefined,
|
||||
},
|
||||
experiments: {
|
||||
topLevelAwait: true,
|
||||
outputModule: true,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -243,4 +253,5 @@ module.exports = {
|
||||
createCastConfig,
|
||||
createHassioConfig,
|
||||
createGalleryConfig,
|
||||
createWebpackConfig,
|
||||
};
|
||||
|
24
gallery/src/data/date-options.ts
Normal file
24
gallery/src/data/date-options.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import type { ControlSelectOption } from "../../../src/components/ha-control-select";
|
||||
|
||||
export const timeOptions: ControlSelectOption[] = [
|
||||
{
|
||||
value: "now",
|
||||
label: "Now",
|
||||
},
|
||||
{
|
||||
value: "00:15:30",
|
||||
label: "12:15:30 AM",
|
||||
},
|
||||
{
|
||||
value: "06:15:30",
|
||||
label: "06:15:30 AM",
|
||||
},
|
||||
{
|
||||
value: "12:15:30",
|
||||
label: "12:15:30 PM",
|
||||
},
|
||||
{
|
||||
value: "18:15:30",
|
||||
label: "06:15:30 PM",
|
||||
},
|
||||
];
|
@ -41,6 +41,7 @@ const triggers = [
|
||||
{ platform: "sun", event: "sunset" },
|
||||
{ platform: "time_pattern" },
|
||||
{ platform: "webhook" },
|
||||
{ platform: "persistent_notification" },
|
||||
{
|
||||
platform: "zone",
|
||||
entity_id: "person.person",
|
||||
@ -50,6 +51,11 @@ const triggers = [
|
||||
{ platform: "tag" },
|
||||
{ platform: "time", at: "15:32" },
|
||||
{ platform: "template" },
|
||||
{ platform: "conversation", command: "Turn on the lights" },
|
||||
{
|
||||
platform: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
{ platform: "event", event_type: "homeassistant_started" },
|
||||
];
|
||||
|
||||
|
@ -19,11 +19,13 @@ import { HaTemplateTrigger } from "../../../../src/panels/config/automation/trig
|
||||
import { HaTimeTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
|
||||
import { HaTimePatternTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
|
||||
import { HaWebhookTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
|
||||
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
|
||||
import { HaZoneTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
|
||||
import { HaDeviceTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
|
||||
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
|
||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
|
||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
|
||||
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
|
||||
|
||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
{
|
||||
@ -72,6 +74,16 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Persistent Notification",
|
||||
triggers: [
|
||||
{
|
||||
platform: "persistent_notification",
|
||||
...HaPersistentNotificationTrigger.defaultConfig,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
name: "Zone",
|
||||
triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
|
||||
@ -101,6 +113,16 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
|
||||
name: "Device Trigger",
|
||||
triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
|
||||
},
|
||||
{
|
||||
name: "Sentence",
|
||||
triggers: [
|
||||
{ platform: "conversation", ...HaConversationTrigger.defaultConfig },
|
||||
{
|
||||
platform: "conversation",
|
||||
command: ["Turn on the lights", "Turn the lights on"],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-automation-editor-trigger")
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Control Circular Slider
|
||||
---
|
178
gallery/src/pages/components/ha-control-circular-slider.ts
Normal file
178
gallery/src/pages/components/ha-control-circular-slider.ts
Normal file
@ -0,0 +1,178 @@
|
||||
import { css, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-circular-slider";
|
||||
import "../../../../src/components/ha-slider";
|
||||
|
||||
@customElement("demo-components-ha-control-circular-slider")
|
||||
export class DemoHaCircularSlider extends LitElement {
|
||||
@state()
|
||||
private current = 22;
|
||||
|
||||
@state()
|
||||
private low = 19;
|
||||
|
||||
@state()
|
||||
private high = 25;
|
||||
|
||||
@state()
|
||||
private changingLow?: number;
|
||||
|
||||
@state()
|
||||
private changingHigh?: number;
|
||||
|
||||
private _lowChanged(ev) {
|
||||
this.low = ev.detail.value;
|
||||
}
|
||||
|
||||
private _lowChanging(ev) {
|
||||
this.changingLow = ev.detail.value;
|
||||
}
|
||||
|
||||
private _highChanged(ev) {
|
||||
this.high = ev.detail.value;
|
||||
}
|
||||
|
||||
private _highChanging(ev) {
|
||||
this.changingHigh = ev.detail.value;
|
||||
}
|
||||
|
||||
private _currentChanged(ev) {
|
||||
this.current = ev.currentTarget.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Config</b></p>
|
||||
<div class="field">
|
||||
<p>Current</p>
|
||||
<ha-slider
|
||||
min="10"
|
||||
max="30"
|
||||
.value=${this.current}
|
||||
@change=${this._currentChanged}
|
||||
pin
|
||||
></ha-slider>
|
||||
<p>${this.current} °C</p>
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Single</b></p>
|
||||
<ha-control-circular-slider
|
||||
@value-changed=${this._lowChanged}
|
||||
@value-changing=${this._lowChanging}
|
||||
.value=${this.low}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
min="10"
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
Low: ${this.low} °C
|
||||
<br />
|
||||
Changing:
|
||||
${this.changingLow != null ? `${this.changingLow} °C` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Inverted</b></p>
|
||||
<ha-control-circular-slider
|
||||
inverted
|
||||
@value-changed=${this._highChanged}
|
||||
@value-changing=${this._highChanging}
|
||||
.value=${this.high}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
min="10"
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
High: ${this.high} °C
|
||||
<br />
|
||||
Changing:
|
||||
${this.changingHigh != null ? `${this.changingHigh} °C` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p class="title"><b>Dual</b></p>
|
||||
<ha-control-circular-slider
|
||||
dual
|
||||
@low-changed=${this._lowChanged}
|
||||
@low-changing=${this._lowChanging}
|
||||
@high-changed=${this._highChanged}
|
||||
@high-changing=${this._highChanging}
|
||||
.low=${this.low}
|
||||
.high=${this.high}
|
||||
.current=${this.current}
|
||||
step="1"
|
||||
min="10"
|
||||
max="30"
|
||||
></ha-control-circular-slider>
|
||||
<div>
|
||||
Low value: ${this.low} °C
|
||||
<br />
|
||||
Low changing:
|
||||
${this.changingLow != null ? `${this.changingLow} °C` : "-"}
|
||||
<br />
|
||||
High value: ${this.high} °C
|
||||
<br />
|
||||
High changing:
|
||||
${this.changingHigh != null ? `${this.changingHigh} °C` : "-"}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
p.title {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
ha-control-circular-slider {
|
||||
--control-circular-slider-color: #ff9800;
|
||||
--control-circular-slider-background: #ff9800;
|
||||
--control-circular-slider-background-opacity: 0.3;
|
||||
}
|
||||
ha-control-circular-slider[inverted] {
|
||||
--control-circular-slider-color: #2196f3;
|
||||
--control-circular-slider-background: #2196f3;
|
||||
}
|
||||
ha-control-circular-slider[dual] {
|
||||
--control-circular-slider-high-color: #2196f3;
|
||||
--control-circular-slider-low-color: #ff9800;
|
||||
--control-circular-slider-background: var(--disabled-color);
|
||||
}
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-control-circular-slider": DemoHaCircularSlider;
|
||||
}
|
||||
}
|
7
gallery/src/pages/date-time/date-time-numeric.markdown
Normal file
7
gallery/src/pages/date-time/date-time-numeric.markdown
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Date-Time Format (Numeric)
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available date-time formats.
|
||||
|
||||
Formatting function: `const formatDateTimeNumeric: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-numeric.ts
Normal file
136
gallery/src/pages/date-time/date-time-numeric.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatDateTimeNumeric } from "../../../../src/common/datetime/format_date_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-date-time-numeric")
|
||||
export class DemoDateTimeDateTimeNumeric extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatDateTimeNumeric(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateTimeNumeric(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateTimeNumeric(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-date-time-numeric": DemoDateTimeDateTimeNumeric;
|
||||
}
|
||||
}
|
7
gallery/src/pages/date-time/date-time-seconds.markdown
Normal file
7
gallery/src/pages/date-time/date-time-seconds.markdown
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Date-Time Format (Seconds)
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available date-time formats.
|
||||
|
||||
Formatting function: `const formatDateTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-seconds.ts
Normal file
136
gallery/src/pages/date-time/date-time-seconds.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatDateTimeWithSeconds } from "../../../../src/common/datetime/format_date_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-date-time-seconds")
|
||||
export class DemoDateTimeDateTimeSeconds extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatDateTimeWithSeconds(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateTimeWithSeconds(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateTimeWithSeconds(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-date-time-seconds": DemoDateTimeDateTimeSeconds;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Date-Time Format (Short w/ Year)
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available date-time formats.
|
||||
|
||||
Formatting function: `const formatShortDateTimeWithYear: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-short-year.ts
Normal file
136
gallery/src/pages/date-time/date-time-short-year.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatShortDateTimeWithYear } from "../../../../src/common/datetime/format_date_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-date-time-short-year")
|
||||
export class DemoDateTimeDateTimeShortYear extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatShortDateTimeWithYear(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatShortDateTimeWithYear(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatShortDateTimeWithYear(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-date-time-short-year": DemoDateTimeDateTimeShortYear;
|
||||
}
|
||||
}
|
7
gallery/src/pages/date-time/date-time-short.markdown
Normal file
7
gallery/src/pages/date-time/date-time-short.markdown
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Date-Time Format (Short)
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available date-time formats.
|
||||
|
||||
Formatting function: `const formatShortDateTime: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time-short.ts
Normal file
136
gallery/src/pages/date-time/date-time-short.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatShortDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-date-time-short")
|
||||
export class DemoDateTimeDateTimeShort extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatShortDateTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatShortDateTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatShortDateTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-date-time-short": DemoDateTimeDateTimeShort;
|
||||
}
|
||||
}
|
7
gallery/src/pages/date-time/date-time.markdown
Normal file
7
gallery/src/pages/date-time/date-time.markdown
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Date-Time Format
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available date-time formats.
|
||||
|
||||
Formatting function: `const formatDateTime: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/date-time.ts
Normal file
136
gallery/src/pages/date-time/date-time.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatDateTime } from "../../../../src/common/datetime/format_date_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-date-time")
|
||||
export class DemoDateTimeDateTime extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatDateTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-date-time": DemoDateTimeDateTime;
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
---
|
||||
title: (Numeric) Date Formatting
|
||||
title: Date Format (Numeric)
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available (numeric) date formats.
|
||||
|
@ -1,27 +1,28 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { HomeAssistant } from "../../../../src/types";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import "@material/mwc-list/mwc-list";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { formatDateNumeric } from "../../../../src/common/datetime/format_date";
|
||||
import "../../../../src/components/ha-card";
|
||||
import {
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
|
||||
@customElement("demo-date-time-date")
|
||||
export class DemoDateTimeDate extends LitElement {
|
||||
@property({ attribute: false }) hass!: HomeAssistant;
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
time_zone: TimeZone.local,
|
||||
first_weekday: FirstWeekday.language,
|
||||
};
|
||||
const date = new Date();
|
||||
@ -41,32 +42,48 @@ export class DemoDateTimeDate extends LitElement {
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatDateNumeric(date, {
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.language,
|
||||
})}
|
||||
${formatDateNumeric(
|
||||
date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateNumeric(date, {
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.DMY,
|
||||
})}
|
||||
${formatDateNumeric(
|
||||
date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.DMY,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateNumeric(date, {
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.MDY,
|
||||
})}
|
||||
${formatDateNumeric(
|
||||
date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.MDY,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatDateNumeric(date, {
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.YMD,
|
||||
})}
|
||||
${formatDateNumeric(
|
||||
date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
date_format: DateFormat.YMD,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
|
7
gallery/src/pages/date-time/time-seconds.markdown
Normal file
7
gallery/src/pages/date-time/time-seconds.markdown
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Time Format (Seconds)
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available time formats.
|
||||
|
||||
Formatting function: `const formatTimeWithSeconds: (dateObj: Date, locale: FrontendLocaleData) => string`
|
135
gallery/src/pages/date-time/time-seconds.ts
Normal file
135
gallery/src/pages/date-time/time-seconds.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatTimeWithSeconds } from "../../../../src/common/datetime/format_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-time-seconds")
|
||||
export class DemoDateTimeTimeSeconds extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatTimeWithSeconds(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatTimeWithSeconds(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatTimeWithSeconds(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-time-seconds": DemoDateTimeTimeSeconds;
|
||||
}
|
||||
}
|
7
gallery/src/pages/date-time/time-weekday.markdown
Normal file
7
gallery/src/pages/date-time/time-weekday.markdown
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Time Format (Weekday)
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available time formats.
|
||||
|
||||
Formatting function: `const formatTimeWeekday: (dateObj: Date, locale: FrontendLocaleData) => string`
|
135
gallery/src/pages/date-time/time-weekday.ts
Normal file
135
gallery/src/pages/date-time/time-weekday.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatTimeWeekday } from "../../../../src/common/datetime/format_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-time-weekday")
|
||||
export class DemoDateTimeTimeWeekday extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatTimeWeekday(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatTimeWeekday(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatTimeWeekday(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-time-weekday": DemoDateTimeTimeWeekday;
|
||||
}
|
||||
}
|
7
gallery/src/pages/date-time/time.markdown
Normal file
7
gallery/src/pages/date-time/time.markdown
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
title: Time Format
|
||||
---
|
||||
|
||||
This pages lists all supported languages with their available time formats.
|
||||
|
||||
Formatting function: `const formatTime: (dateObj: Date, locale: FrontendLocaleData) => string`
|
136
gallery/src/pages/date-time/time.ts
Normal file
136
gallery/src/pages/date-time/time.ts
Normal file
@ -0,0 +1,136 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-select";
|
||||
import { translationMetadata } from "../../../../src/resources/translations-metadata";
|
||||
import { formatTime } from "../../../../src/common/datetime/format_time";
|
||||
import { timeOptions } from "../../data/date-options";
|
||||
import { demoConfig } from "../../../../src/fake_data/demo_config";
|
||||
import {
|
||||
FrontendLocaleData,
|
||||
NumberFormat,
|
||||
TimeFormat,
|
||||
DateFormat,
|
||||
FirstWeekday,
|
||||
TimeZone,
|
||||
} from "../../../../src/data/translation";
|
||||
|
||||
@customElement("demo-date-time-time")
|
||||
export class DemoDateTimeTime extends LitElement {
|
||||
@state() private selection?: string = "now";
|
||||
|
||||
@state() private date: Date = new Date();
|
||||
|
||||
handleValueChanged(e: CustomEvent) {
|
||||
this.selection = e.detail.value as string;
|
||||
this.date = new Date();
|
||||
if (this.selection !== "now") {
|
||||
const [hours, minutes, seconds] = this.selection.split(":").map(Number);
|
||||
this.date.setHours(hours);
|
||||
this.date.setMinutes(minutes);
|
||||
this.date.setSeconds(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const defaultLocale: FrontendLocaleData = {
|
||||
language: "en",
|
||||
number_format: NumberFormat.language,
|
||||
time_format: TimeFormat.language,
|
||||
date_format: DateFormat.language,
|
||||
first_weekday: FirstWeekday.language,
|
||||
time_zone: TimeZone.local,
|
||||
};
|
||||
return html`
|
||||
<ha-control-select
|
||||
.value=${this.selection}
|
||||
.options=${timeOptions}
|
||||
@value-changed=${this.handleValueChanged}
|
||||
>
|
||||
</ha-control-select>
|
||||
<mwc-list>
|
||||
<div class="container header">
|
||||
<div>Language</div>
|
||||
<div class="center">Default (lang)</div>
|
||||
<div class="center">12 Hours</div>
|
||||
<div class="center">24 Hours</div>
|
||||
</div>
|
||||
${Object.entries(translationMetadata.translations)
|
||||
.filter(([key, _]) => key !== "test")
|
||||
.map(
|
||||
([key, value]) => html`
|
||||
<div class="container">
|
||||
<div>${value.nativeName}</div>
|
||||
<div class="center">
|
||||
${formatTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.language,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.am_pm,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
<div class="center">
|
||||
${formatTime(
|
||||
this.date,
|
||||
{
|
||||
...defaultLocale,
|
||||
language: key,
|
||||
time_format: TimeFormat.twenty_four,
|
||||
},
|
||||
demoConfig
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</mwc-list>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-select {
|
||||
max-width: 800px;
|
||||
margin: 12px auto;
|
||||
}
|
||||
.header {
|
||||
font-weight: bold;
|
||||
}
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 12px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.container > div {
|
||||
flex-grow: 1;
|
||||
width: 20%;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-date-time-time": DemoDateTimeTime;
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ const CONFIGS = [
|
||||
heading: "markdown-it demo",
|
||||
config: `
|
||||
- type: markdown
|
||||
content: >-
|
||||
content: |
|
||||
# h1 Heading 8-)
|
||||
|
||||
## h2 Heading
|
||||
@ -65,6 +65,15 @@ const CONFIGS = [
|
||||
>> ...by using additional greater-than signs right next to each other...
|
||||
> > > ...or with spaces between arrows.
|
||||
|
||||
> **Warning** Hey there
|
||||
> This is a warning with a title
|
||||
|
||||
> **Note**
|
||||
> This is a note
|
||||
|
||||
> **Note**
|
||||
> This is a multiline note
|
||||
> Lorem ipsum...
|
||||
|
||||
## Lists
|
||||
|
||||
|
@ -135,6 +135,9 @@ const ENTITIES: HassEntity[] = [
|
||||
createEntity("climate.fan_only", "fan_only"),
|
||||
createEntity("climate.auto_idle", "auto", undefined, { hvac_action: "idle" }),
|
||||
createEntity("climate.auto_off", "auto", undefined, { hvac_action: "off" }),
|
||||
createEntity("climate.auto_preheating", "auto", undefined, {
|
||||
hvac_action: "preheating",
|
||||
}),
|
||||
createEntity("climate.auto_heating", "auto", undefined, {
|
||||
hvac_action: "heating",
|
||||
}),
|
||||
@ -354,6 +357,7 @@ export class DemoEntityState extends LitElement {
|
||||
hass.localize,
|
||||
entry.stateObj,
|
||||
hass.locale,
|
||||
hass.config,
|
||||
hass.entities
|
||||
)}`,
|
||||
},
|
||||
|
@ -114,11 +114,22 @@ class HassioAddonInfo extends LitElement {
|
||||
|
||||
@state() private _error?: string;
|
||||
|
||||
private _fetchDataTimeout?: number;
|
||||
|
||||
private _addonStoreInfo = memoizeOne(
|
||||
(slug: string, storeAddons: StoreAddon[]) =>
|
||||
storeAddons.find((addon) => addon.slug === slug)
|
||||
);
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const addonStoreInfo =
|
||||
!this.addon.detached && !this.addon.available
|
||||
@ -592,7 +603,10 @@ class HassioAddonInfo extends LitElement {
|
||||
</ha-progress-button>
|
||||
`
|
||||
: html`
|
||||
<ha-progress-button @click=${this._startClicked}>
|
||||
<ha-progress-button
|
||||
@click=${this._startClicked}
|
||||
.progress=${this.addon.state === "startup"}
|
||||
>
|
||||
${this.supervisor.localize("addon.dashboard.start")}
|
||||
</ha-progress-button>
|
||||
`
|
||||
@ -672,9 +686,36 @@ class HassioAddonInfo extends LitElement {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("addon")) {
|
||||
this._loadData();
|
||||
if (
|
||||
!this._fetchDataTimeout &&
|
||||
this.addon &&
|
||||
"state" in this.addon &&
|
||||
this.addon.state === "startup"
|
||||
) {
|
||||
// Addon is starting up, wait for it to start
|
||||
this._scheduleDataUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _scheduleDataUpdate() {
|
||||
this._fetchDataTimeout = window.setTimeout(async () => {
|
||||
const addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
|
||||
if (addon.state !== "startup") {
|
||||
this._fetchDataTimeout = undefined;
|
||||
this.addon = addon;
|
||||
const eventdata = {
|
||||
success: true,
|
||||
response: undefined,
|
||||
path: "start",
|
||||
};
|
||||
fireEvent(this, "hass-api-called", eventdata);
|
||||
} else {
|
||||
this._scheduleDataUpdate();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private async _loadData(): Promise<void> {
|
||||
if ("state" in this.addon && this.addon.state === "started") {
|
||||
this._metrics = await fetchHassioStats(
|
||||
|
@ -136,6 +136,15 @@ export class HassioBackups extends LitElement {
|
||||
sortable: true,
|
||||
template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB",
|
||||
},
|
||||
location: {
|
||||
title: this.supervisor.localize("backup.location"),
|
||||
width: "15%",
|
||||
hidden: narrow,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
template: (entry: string | null) =>
|
||||
entry || this.supervisor.localize("backup.data_disk"),
|
||||
},
|
||||
date: {
|
||||
title: this.supervisor.localize("backup.created"),
|
||||
width: "15%",
|
||||
|
@ -143,7 +143,11 @@ export class SupervisorBackupContent extends LitElement {
|
||||
: this._localize("partial_backup")}
|
||||
(${Math.ceil(this.backup.size * 10) / 10 + " MB"})<br />
|
||||
${this.hass
|
||||
? formatDateTime(new Date(this.backup.date), this.hass.locale)
|
||||
? formatDateTime(
|
||||
new Date(this.backup.date),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: this.backup.date}
|
||||
</div>`
|
||||
: html`<paper-input
|
||||
@ -336,7 +340,9 @@ export class SupervisorBackupContent extends LitElement {
|
||||
const data: any = {};
|
||||
|
||||
if (!this.backup) {
|
||||
data.name = this.backupName || formatDate(new Date(), this.hass.locale);
|
||||
data.name =
|
||||
this.backupName ||
|
||||
formatDate(new Date(), this.hass.locale, this.hass.config);
|
||||
}
|
||||
|
||||
if (this.backupHasPassword) {
|
||||
|
@ -137,6 +137,9 @@ class HassioAddons extends LitElement {
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
}
|
||||
.content {
|
||||
margin-bottom: 72px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import "../../../src/components/ha-icon-button";
|
||||
import {
|
||||
fetchHassioAddonInfo,
|
||||
HassioAddonDetails,
|
||||
startHassioAddon,
|
||||
} from "../../../src/data/hassio/addon";
|
||||
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
|
||||
import {
|
||||
@ -23,7 +24,10 @@ import {
|
||||
validateHassioSession,
|
||||
} from "../../../src/data/hassio/ingress";
|
||||
import { Supervisor } from "../../../src/data/supervisor/supervisor";
|
||||
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../src/dialogs/generic/show-dialog-box";
|
||||
import "../../../src/layouts/hass-loading-screen";
|
||||
import "../../../src/layouts/hass-subpage";
|
||||
import { HomeAssistant, Route } from "../../../src/types";
|
||||
@ -45,6 +49,8 @@ class HassioIngressView extends LitElement {
|
||||
|
||||
private _sessionKeepAlive?: number;
|
||||
|
||||
private _fetchDataTimeout?: number;
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
@ -52,16 +58,21 @@ class HassioIngressView extends LitElement {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
this._sessionKeepAlive = undefined;
|
||||
}
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this._addon) {
|
||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
const iframe = html`<iframe
|
||||
title=${this._addon.name}
|
||||
src=${this._addon.ingress_url!}
|
||||
@load=${this._checkLoaded}
|
||||
>
|
||||
</iframe>`;
|
||||
|
||||
@ -132,10 +143,10 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const addon = this.route.path.substr(1);
|
||||
const addon = this.route.path.substring(1);
|
||||
|
||||
const oldRoute = changedProps.get("route") as this["route"] | undefined;
|
||||
const oldAddon = oldRoute ? oldRoute.path.substr(1) : undefined;
|
||||
const oldAddon = oldRoute ? oldRoute.path.substring(1) : undefined;
|
||||
|
||||
if (addon && addon !== oldAddon) {
|
||||
this._fetchData(addon);
|
||||
@ -145,33 +156,23 @@ class HassioIngressView extends LitElement {
|
||||
private async _fetchData(addonSlug: string) {
|
||||
const createSessionPromise = createHassioSession(this.hass);
|
||||
|
||||
let addon;
|
||||
let addon: HassioAddonDetails;
|
||||
|
||||
try {
|
||||
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
|
||||
} catch (err: any) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to fetch add-on info to start Ingress",
|
||||
text: this.supervisor.localize("ingress.error_addon_info"),
|
||||
title: "Supervisor",
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
navigate("/hassio/store", { replace: true });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.ingress_url) {
|
||||
if (!addon.version) {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on does not support Ingress",
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
await showAlertDialog(this, {
|
||||
text: "Add-on is not running. Please start it first",
|
||||
text: this.supervisor.localize("ingress.error_addon_not_installed"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@ -179,13 +180,74 @@ class HassioIngressView extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
let session;
|
||||
if (!addon.ingress_url) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_addon_not_supported"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
history.back();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addon.state || !["startup", "started"].includes(addon.state)) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_addon_not_running"),
|
||||
title: addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.start_addon"),
|
||||
dismissText: this.supervisor.localize("common.no"),
|
||||
});
|
||||
if (confirm) {
|
||||
try {
|
||||
await startHassioAddon(this.hass, addonSlug);
|
||||
fireEvent(this, "supervisor-collection-refresh", {
|
||||
collection: "addon",
|
||||
});
|
||||
this._fetchData(addonSlug);
|
||||
return;
|
||||
} catch (e) {
|
||||
await showAlertDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_starting_addon"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/logs`, { replace: true });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await nextRender();
|
||||
navigate(`/hassio/addon/${addon.slug}/info`, { replace: true });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (addon.state === "startup") {
|
||||
// Addon is starting up, wait for it to start
|
||||
this._fetchDataTimeout = window.setTimeout(() => {
|
||||
this._fetchData(addonSlug);
|
||||
}, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
if (addon.state !== "started") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._fetchDataTimeout) {
|
||||
clearInterval(this._fetchDataTimeout);
|
||||
this._fetchDataTimeout = undefined;
|
||||
}
|
||||
|
||||
let session: string;
|
||||
|
||||
try {
|
||||
session = await createSessionPromise;
|
||||
} catch (err: any) {
|
||||
if (this._sessionKeepAlive) {
|
||||
clearInterval(this._sessionKeepAlive);
|
||||
}
|
||||
await showAlertDialog(this, {
|
||||
text: "Unable to create an Ingress session",
|
||||
text: this.supervisor.localize("ingress.error_creating_session"),
|
||||
title: addon.name,
|
||||
});
|
||||
await nextRender();
|
||||
@ -207,6 +269,31 @@ class HassioIngressView extends LitElement {
|
||||
this._addon = addon;
|
||||
}
|
||||
|
||||
private _checkLoaded(ev): void {
|
||||
if (!this._addon) {
|
||||
return;
|
||||
}
|
||||
if (ev.target.contentDocument.body.textContent === "502: Bad Gateway") {
|
||||
showConfirmationDialog(this, {
|
||||
text: this.supervisor.localize("ingress.error_addon_not_ready"),
|
||||
title: this._addon.name,
|
||||
confirmText: this.supervisor.localize("ingress.retry"),
|
||||
dismissText: this.supervisor.localize("common.no"),
|
||||
confirm: async () => {
|
||||
const addon = this._addon;
|
||||
this._addon = undefined;
|
||||
await Promise.all([
|
||||
this.updateComplete,
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, 500);
|
||||
}),
|
||||
]);
|
||||
this._addon = addon;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _toggleMenu(): void {
|
||||
fireEvent(this, "hass-toggle-menu");
|
||||
}
|
||||
|
97
package.json
97
package.json
@ -25,33 +25,34 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.22.3",
|
||||
"@babel/runtime": "7.22.5",
|
||||
"@braintree/sanitize-url": "6.0.2",
|
||||
"@codemirror/autocomplete": "6.7.1",
|
||||
"@codemirror/autocomplete": "6.8.1",
|
||||
"@codemirror/commands": "6.2.4",
|
||||
"@codemirror/language": "6.7.0",
|
||||
"@codemirror/language": "6.8.0",
|
||||
"@codemirror/legacy-modes": "6.3.2",
|
||||
"@codemirror/search": "6.4.0",
|
||||
"@codemirror/search": "6.5.0",
|
||||
"@codemirror/state": "6.2.1",
|
||||
"@codemirror/view": "6.12.0",
|
||||
"@codemirror/view": "6.14.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "6.8.0",
|
||||
"@formatjs/intl-displaynames": "6.3.2",
|
||||
"@formatjs/intl-getcanonicallocales": "2.2.0",
|
||||
"@formatjs/intl-locale": "3.3.0",
|
||||
"@formatjs/intl-numberformat": "8.5.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.2",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.2",
|
||||
"@formatjs/intl-datetimeformat": "6.10.0",
|
||||
"@formatjs/intl-displaynames": "6.5.0",
|
||||
"@formatjs/intl-getcanonicallocales": "2.2.1",
|
||||
"@formatjs/intl-listformat": "7.4.0",
|
||||
"@formatjs/intl-locale": "3.3.2",
|
||||
"@formatjs/intl-numberformat": "8.7.0",
|
||||
"@formatjs/intl-pluralrules": "5.2.4",
|
||||
"@formatjs/intl-relativetimeformat": "11.2.4",
|
||||
"@fullcalendar/core": "6.1.8",
|
||||
"@fullcalendar/daygrid": "6.1.8",
|
||||
"@fullcalendar/interaction": "6.1.8",
|
||||
"@fullcalendar/list": "6.1.8",
|
||||
"@fullcalendar/timegrid": "6.1.8",
|
||||
"@lezer/highlight": "1.1.6",
|
||||
"@lit-labs/context": "0.3.2",
|
||||
"@lit-labs/context": "0.3.3",
|
||||
"@lit-labs/motion": "1.0.3",
|
||||
"@lit-labs/virtualizer": "2.0.2",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.2",
|
||||
"@lit-labs/virtualizer": "2.0.3",
|
||||
"@lrnwebcomponents/simple-tooltip": "7.0.5",
|
||||
"@material/chips": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/mwc-button": "0.27.0",
|
||||
@ -77,7 +78,7 @@
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "=1.0.0-pre.9",
|
||||
"@material/web": "=1.0.0-pre.11",
|
||||
"@mdi/js": "7.2.96",
|
||||
"@mdi/svg": "7.2.96",
|
||||
"@polymer/app-layout": "3.1.0",
|
||||
@ -92,8 +93,8 @@
|
||||
"@polymer/paper-toast": "3.0.1",
|
||||
"@polymer/polymer": "3.5.1",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@vaadin/combo-box": "24.0.8",
|
||||
"@vaadin/vaadin-themable-mixin": "24.0.8",
|
||||
"@vaadin/combo-box": "24.1.1",
|
||||
"@vaadin/vaadin-themable-mixin": "24.1.1",
|
||||
"@vibrant/color": "3.2.1-alpha.1",
|
||||
"@vibrant/core": "3.2.1-alpha.1",
|
||||
"@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
|
||||
@ -103,7 +104,7 @@
|
||||
"app-datepicker": "5.1.1",
|
||||
"chart.js": "3.3.2",
|
||||
"comlink": "4.4.1",
|
||||
"core-js": "3.30.2",
|
||||
"core-js": "3.31.0",
|
||||
"cropperjs": "1.5.13",
|
||||
"date-fns": "2.30.0",
|
||||
"date-fns-tz": "2.0.0",
|
||||
@ -111,10 +112,10 @@
|
||||
"deep-freeze": "0.0.1",
|
||||
"fuse.js": "6.6.2",
|
||||
"google-timezones-json": "1.1.0",
|
||||
"hls.js": "1.4.4",
|
||||
"home-assistant-js-websocket": "8.0.1",
|
||||
"hls.js": "1.4.6",
|
||||
"home-assistant-js-websocket": "8.1.0",
|
||||
"idb-keyval": "6.2.1",
|
||||
"intl-messageformat": "10.3.5",
|
||||
"intl-messageformat": "10.5.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "1.0.4",
|
||||
@ -131,9 +132,9 @@
|
||||
"rrule": "2.7.2",
|
||||
"sortablejs": "1.15.0",
|
||||
"superstruct": "1.0.3",
|
||||
"tinykeys": "1.4.0",
|
||||
"tsparticles-engine": "2.9.3",
|
||||
"tsparticles-preset-links": "2.9.3",
|
||||
"tinykeys": "2.1.0",
|
||||
"tsparticles-engine": "2.10.1",
|
||||
"tsparticles-preset-links": "2.10.1",
|
||||
"unfetch": "5.0.0",
|
||||
"vis-data": "7.1.6",
|
||||
"vis-network": "9.1.6",
|
||||
@ -149,18 +150,18 @@
|
||||
"xss": "1.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.22.1",
|
||||
"@babel/plugin-proposal-decorators": "7.22.3",
|
||||
"@babel/plugin-transform-runtime": "7.22.4",
|
||||
"@babel/preset-env": "7.22.4",
|
||||
"@babel/preset-typescript": "7.21.5",
|
||||
"@babel/core": "7.22.5",
|
||||
"@babel/plugin-proposal-decorators": "7.22.5",
|
||||
"@babel/plugin-transform-runtime": "7.22.5",
|
||||
"@babel/preset-env": "7.22.5",
|
||||
"@babel/preset-typescript": "7.22.5",
|
||||
"@koa/cors": "4.0.0",
|
||||
"@octokit/auth-oauth-device": "4.0.4",
|
||||
"@octokit/plugin-retry": "5.0.0",
|
||||
"@octokit/rest": "19.0.11",
|
||||
"@octokit/auth-oauth-device": "5.0.2",
|
||||
"@octokit/plugin-retry": "5.0.4",
|
||||
"@octokit/rest": "19.0.13",
|
||||
"@open-wc/dev-server-hmr": "0.1.4",
|
||||
"@rollup/plugin-babel": "6.0.3",
|
||||
"@rollup/plugin-commonjs": "25.0.0",
|
||||
"@rollup/plugin-commonjs": "25.0.2",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-node-resolve": "15.1.0",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
@ -172,7 +173,7 @@
|
||||
"@types/html-minifier-terser": "7.0.0",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
"@types/leaflet": "1.9.3",
|
||||
"@types/leaflet-draw": "1.0.6",
|
||||
"@types/leaflet-draw": "1.0.7",
|
||||
"@types/marked": "4.3.1",
|
||||
"@types/mocha": "10.0.1",
|
||||
"@types/qrcode": "1.5.0",
|
||||
@ -180,15 +181,15 @@
|
||||
"@types/sortablejs": "1.15.1",
|
||||
"@types/tar": "6.1.5",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.8",
|
||||
"@typescript-eslint/parser": "5.59.8",
|
||||
"@typescript-eslint/eslint-plugin": "5.60.0",
|
||||
"@typescript-eslint/parser": "5.60.0",
|
||||
"@web/dev-server": "0.1.38",
|
||||
"@web/dev-server-rollup": "0.4.1",
|
||||
"babel-loader": "9.1.2",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"chai": "4.3.7",
|
||||
"del": "7.0.0",
|
||||
"eslint": "8.42.0",
|
||||
"eslint": "8.43.0",
|
||||
"eslint-config-airbnb-base": "15.0.0",
|
||||
"eslint-config-airbnb-typescript": "17.0.0",
|
||||
"eslint-config-prettier": "8.8.0",
|
||||
@ -196,13 +197,13 @@
|
||||
"eslint-plugin-disable": "2.0.3",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-lit": "1.8.3",
|
||||
"eslint-plugin-lit-a11y": "2.4.1",
|
||||
"eslint-plugin-lit-a11y": "3.0.0",
|
||||
"eslint-plugin-unused-imports": "2.0.0",
|
||||
"eslint-plugin-wc": "1.5.0",
|
||||
"esprima": "4.0.1",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.1.1",
|
||||
"glob": "10.2.6",
|
||||
"glob": "10.3.0",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-flatmap": "1.0.2",
|
||||
"gulp-json-transform": "0.4.8",
|
||||
@ -214,7 +215,7 @@
|
||||
"instant-mocha": "1.5.1",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "13.2.2",
|
||||
"lit-analyzer": "1.2.1",
|
||||
"lit-analyzer": "2.0.0-pre.3",
|
||||
"lodash.template": "4.5.0",
|
||||
"magic-string": "0.30.0",
|
||||
"map-stream": "0.0.7",
|
||||
@ -227,20 +228,20 @@
|
||||
"rollup": "2.79.1",
|
||||
"rollup-plugin-string": "3.0.0",
|
||||
"rollup-plugin-terser": "7.0.2",
|
||||
"rollup-plugin-visualizer": "5.9.0",
|
||||
"rollup-plugin-visualizer": "5.9.2",
|
||||
"serve-handler": "6.1.5",
|
||||
"sinon": "15.1.0",
|
||||
"sinon": "15.2.0",
|
||||
"source-map-url": "0.4.1",
|
||||
"systemjs": "6.14.1",
|
||||
"tar": "6.1.15",
|
||||
"terser-webpack-plugin": "5.3.9",
|
||||
"ts-lit-plugin": "1.2.1",
|
||||
"typescript": "4.9.5",
|
||||
"ts-lit-plugin": "2.0.0-pre.1",
|
||||
"typescript": "5.1.3",
|
||||
"vinyl-buffer": "1.0.1",
|
||||
"vinyl-source-stream": "2.0.0",
|
||||
"webpack": "5.84.1",
|
||||
"webpack-cli": "5.1.1",
|
||||
"webpack-dev-server": "4.15.0",
|
||||
"webpack": "5.88.0",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "4.15.1",
|
||||
"webpack-manifest-plugin": "5.0.0",
|
||||
"webpackbar": "5.0.2",
|
||||
"workbox-build": "7.0.0"
|
||||
|
@ -1,17 +1,17 @@
|
||||
[build-system]
|
||||
requires = ["setuptools~=62.3", "wheel~=0.37.1"]
|
||||
requires = ["setuptools~=68.0", "wheel~=0.40.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20230608.0"
|
||||
version = "20230628.0"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "The Home Assistant frontend"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{name = "The Home Assistant Authors", email = "hello@home-assistant.io"}
|
||||
]
|
||||
requires-python = ">=3.4.0"
|
||||
requires-python = ">=3.10.0"
|
||||
|
||||
[project.urls]
|
||||
"Homepage" = "https://github.com/home-assistant/frontend"
|
||||
|
@ -19,14 +19,20 @@
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": ["MDC packages are pinned to the same version as MWC"],
|
||||
"description": "MDC packages are pinned to the same version as MWC",
|
||||
"extends": ["monorepo:material-components-web"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": ["Vue is only used by date range which is only v2"],
|
||||
"description": "Vue is only used by date range which is only v2",
|
||||
"matchPackageNames": ["vue"],
|
||||
"allowedVersions": "< 3"
|
||||
},
|
||||
{
|
||||
"description": "Group tsparticles engine and presets",
|
||||
"groupName": "tsparticles",
|
||||
"matchPackageNames": ["tsparticles-engine"],
|
||||
"matchPackagePrefixes": ["tsparticles-preset-"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -8,9 +8,9 @@ cd "$(dirname "$0")/.."
|
||||
|
||||
# Install/upgrade node when inside devcontainer
|
||||
if [[ -n "$DEVCONTAINER" ]]; then
|
||||
nodeCurrent=$(nvm version default || echo "")
|
||||
nodeCurrent=$(nvm version default || :)
|
||||
nodeLatest=$(nvm version-remote "$(cat .nvmrc)")
|
||||
if [[ -z "$nodeCurrent" ]]; then
|
||||
if [[ -z "$nodeCurrent" || "$nodeCurrent" == "N/A" ]]; then
|
||||
nvm install
|
||||
elif [[ "$nodeCurrent" != "$nodeLatest" ]]; then
|
||||
nvm install --reinstall-packages-from="$nodeCurrent" --default
|
||||
|
@ -1,2 +0,0 @@
|
||||
# Setuptools v62.3 doesn't support editable installs with just 'pyproject.toml' (PEP 660).
|
||||
# Keep this file until it does!
|
@ -33,6 +33,7 @@ import {
|
||||
mdiGoogleCirclesCommunities,
|
||||
mdiHomeAssistant,
|
||||
mdiHomeAutomation,
|
||||
mdiImage,
|
||||
mdiImageFilterFrames,
|
||||
mdiLightbulb,
|
||||
mdiLightningBolt,
|
||||
@ -90,6 +91,7 @@ export const FIXED_DOMAIN_ICONS = {
|
||||
group: mdiGoogleCirclesCommunities,
|
||||
homeassistant: mdiHomeAssistant,
|
||||
homekit: mdiHomeAutomation,
|
||||
image: mdiImage,
|
||||
image_processing: mdiImageFilterFrames,
|
||||
input_button: mdiGestureTapButton,
|
||||
input_datetime: mdiCalendarClock,
|
||||
@ -180,6 +182,7 @@ export const DOMAINS_WITH_CARD = [
|
||||
"input_select",
|
||||
"input_number",
|
||||
"input_text",
|
||||
"humidifier",
|
||||
"lock",
|
||||
"media_player",
|
||||
"number",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { isSameDay, isSameYear } from "date-fns";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import {
|
||||
formatShortDateTime,
|
||||
@ -9,15 +10,16 @@ import { formatTime } from "./format_time";
|
||||
export const absoluteTime = (
|
||||
from: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
to?: Date
|
||||
): string => {
|
||||
const _to = to ?? new Date();
|
||||
|
||||
if (isSameDay(from, _to)) {
|
||||
return formatTime(from, locale);
|
||||
return formatTime(from, locale, config);
|
||||
}
|
||||
if (isSameYear(from, _to)) {
|
||||
return formatShortDateTime(from, locale);
|
||||
return formatShortDateTime(from, locale, config);
|
||||
}
|
||||
return formatShortDateTimeWithYear(from, locale);
|
||||
return formatShortDateTimeWithYear(from, locale, config);
|
||||
};
|
||||
|
25
src/common/datetime/calc_date.ts
Normal file
25
src/common/datetime/calc_date.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { utcToZonedTime, zonedTimeToUtc } from "date-fns-tz";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||
|
||||
const calcZonedDate = (
|
||||
date: Date,
|
||||
tz: string,
|
||||
fn: (date: Date, options?: any) => Date,
|
||||
options?
|
||||
) => {
|
||||
const inputZoned = utcToZonedTime(date, tz);
|
||||
const fnZoned = fn(inputZoned, options);
|
||||
return zonedTimeToUtc(fnZoned, tz);
|
||||
};
|
||||
|
||||
export const calcDate = (
|
||||
date: Date,
|
||||
fn: (date: Date, options?: any) => Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
options?
|
||||
) =>
|
||||
locale.time_zone === TimeZone.server
|
||||
? calcZonedDate(date, config.time_zone, fn, options)
|
||||
: fn(date, options);
|
@ -1,3 +1,4 @@
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData, DateFormat } from "../../data/translation";
|
||||
import "../../resources/intl-polyfill";
|
||||
@ -5,37 +6,44 @@ import "../../resources/intl-polyfill";
|
||||
// Tuesday, August 10
|
||||
export const formatDateWeekdayDay = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatDateWeekdayDayMem(locale).format(dateObj);
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateWeekdayDayMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayDayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// August 10, 2021
|
||||
export const formatDate = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateMem(locale).format(dateObj);
|
||||
export const formatDate = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// 10/08/2021
|
||||
export const formatDateNumeric = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => {
|
||||
const formatter = formatDateNumericMem(locale);
|
||||
const formatter = formatDateNumericMem(locale, config.time_zone);
|
||||
|
||||
if (
|
||||
locale.date_format === DateFormat.language ||
|
||||
@ -67,83 +75,120 @@ export const formatDateNumeric = (
|
||||
return formats[locale.date_format];
|
||||
};
|
||||
|
||||
const formatDateNumericMem = memoizeOne((locale: FrontendLocaleData) => {
|
||||
const localeString =
|
||||
locale.date_format === DateFormat.system ? undefined : locale.language;
|
||||
const formatDateNumericMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) => {
|
||||
const localeString =
|
||||
locale.date_format === DateFormat.system ? undefined : locale.language;
|
||||
|
||||
if (
|
||||
locale.date_format === DateFormat.language ||
|
||||
locale.date_format === DateFormat.system
|
||||
) {
|
||||
return new Intl.DateTimeFormat(localeString, {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
locale.date_format === DateFormat.language ||
|
||||
locale.date_format === DateFormat.system
|
||||
) {
|
||||
return new Intl.DateTimeFormat(localeString, {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return new Intl.DateTimeFormat(localeString, {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
});
|
||||
});
|
||||
);
|
||||
|
||||
// Aug 10
|
||||
export const formatDateShort = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateShortMem(locale).format(dateObj);
|
||||
export const formatDateShort = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateShortMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateShortMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
day: "numeric",
|
||||
month: "short",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// August 2021
|
||||
export const formatDateMonthYear = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatDateMonthYearMem(locale).format(dateObj);
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateMonthYearMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateMonthYearMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// August
|
||||
export const formatDateMonth = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateMonthMem(locale).format(dateObj);
|
||||
export const formatDateMonth = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateMonthMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateMonthMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
month: "long",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// 2021
|
||||
export const formatDateYear = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateYearMem(locale).format(dateObj);
|
||||
export const formatDateYear = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateYearMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateYearMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// Monday
|
||||
export const formatDateWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateWeekdayMem(locale).format(dateObj);
|
||||
export const formatDateWeekday = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateWeekdayMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// Mon
|
||||
export const formatDateWeekdayShort = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateWeekdayShortMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateWeekdayShortMem = memoizeOne(
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "short",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
@ -1,102 +1,99 @@
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import "../../resources/intl-polyfill";
|
||||
import { useAmPm } from "./use_am_pm";
|
||||
import { formatDateNumeric } from "./format_date";
|
||||
import { formatTime } from "./format_time";
|
||||
import { useAmPm } from "./use_am_pm";
|
||||
|
||||
// August 9, 2021, 8:23 AM
|
||||
export const formatDateTime = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatDateTimeMem(locale).format(dateObj);
|
||||
export const formatDateTime = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateTimeMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// Aug 9, 2021, 8:23 AM
|
||||
export const formatShortDateTimeWithYear = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatShortDateTimeWithYearMem(locale).format(dateObj);
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatShortDateTimeWithYearMem(locale, config.time_zone).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),
|
||||
}
|
||||
)
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// Aug 9, 8:23 AM
|
||||
export const formatShortDateTime = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatShortDateTimeMem(locale).format(dateObj);
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatShortDateTimeMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatShortDateTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// August 9, 2021, 8:23:15 AM
|
||||
export const formatDateTimeWithSeconds = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatDateTimeWithSecondsMem(locale).format(dateObj);
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatDateTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatDateTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// 9/8/2021, 8:23 AM
|
||||
export const formatDateTimeNumeric = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => `${formatDateNumeric(dateObj, locale)}, ${formatTime(dateObj, locale)}`;
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) =>
|
||||
`${formatDateNumeric(dateObj, locale, config)}, ${formatTime(
|
||||
dateObj,
|
||||
locale,
|
||||
config
|
||||
)}`;
|
||||
|
@ -1,76 +1,76 @@
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import "../../resources/intl-polyfill";
|
||||
import { useAmPm } from "./use_am_pm";
|
||||
|
||||
// 9:15 PM || 21:15
|
||||
export const formatTime = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatTimeMem(locale).format(dateObj);
|
||||
export const formatTime = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatTimeMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTimeMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// 9:15:24 PM || 21:15:24
|
||||
export const formatTimeWithSeconds = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData
|
||||
) => formatTimeWithSecondsMem(locale).format(dateObj);
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatTimeWithSecondsMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTimeWithSecondsMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// Tuesday 7:00 PM || Tuesday 19:00
|
||||
export const formatTimeWeekday = (dateObj: Date, locale: FrontendLocaleData) =>
|
||||
formatTimeWeekdayMem(locale).format(dateObj);
|
||||
export const formatTimeWeekday = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatTimeWeekdayMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTimeWeekdayMem = memoizeOne(
|
||||
(locale: FrontendLocaleData) =>
|
||||
new Intl.DateTimeFormat(
|
||||
locale.language === "en" && !useAmPm(locale)
|
||||
? "en-u-hc-h23"
|
||||
: locale.language,
|
||||
{
|
||||
weekday: "long",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: useAmPm(locale),
|
||||
}
|
||||
)
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
new Intl.DateTimeFormat(locale.language, {
|
||||
weekday: "long",
|
||||
hour: useAmPm(locale) ? "numeric" : "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: useAmPm(locale) ? "h12" : "h23",
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
// 21:15
|
||||
export const formatTime24h = (dateObj: Date) =>
|
||||
formatTime24hMem().format(dateObj);
|
||||
export const formatTime24h = (
|
||||
dateObj: Date,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig
|
||||
) => formatTime24hMem(locale, config.time_zone).format(dateObj);
|
||||
|
||||
const formatTime24hMem = memoizeOne(
|
||||
() =>
|
||||
(locale: FrontendLocaleData, serverTimeZone: string) =>
|
||||
// en-GB to fix Chrome 24:59 to 0:59 https://stackoverflow.com/a/60898146
|
||||
new Intl.DateTimeFormat("en-GB", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
timeZone: locale.time_zone === "server" ? serverTimeZone : undefined,
|
||||
})
|
||||
);
|
||||
|
@ -8,8 +8,10 @@ export const useAmPm = memoizeOne((locale: FrontendLocaleData): boolean => {
|
||||
) {
|
||||
const testLanguage =
|
||||
locale.time_format === TimeFormat.language ? locale.language : undefined;
|
||||
const test = new Date().toLocaleString(testLanguage);
|
||||
return test.includes("AM") || test.includes("PM");
|
||||
const test = new Date("January 1, 2023 22:00:00").toLocaleString(
|
||||
testLanguage
|
||||
);
|
||||
return test.includes("10");
|
||||
}
|
||||
|
||||
return locale.time_format === TimeFormat.am_pm;
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { PropertyDeclaration, ReactiveElement } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { InternalPropertyDeclaration } from "lit/decorators";
|
||||
import type { ClassElement } from "../../types";
|
||||
|
||||
type Callback = (oldValue: any, newValue: any) => void;
|
||||
|
||||
class Storage {
|
||||
constructor(subscribe = true, storage = window.localStorage) {
|
||||
class StorageClass {
|
||||
constructor(storage = window.localStorage) {
|
||||
this.storage = storage;
|
||||
if (!subscribe) {
|
||||
if (storage !== window.localStorage) {
|
||||
// storage events only work for localStorage
|
||||
return;
|
||||
}
|
||||
window.addEventListener("storage", (ev: StorageEvent) => {
|
||||
@ -77,6 +79,7 @@ class Storage {
|
||||
}
|
||||
|
||||
public setValue(storageKey: string, value: any): any {
|
||||
const oldValue = this._storage[storageKey];
|
||||
this._storage[storageKey] = value;
|
||||
try {
|
||||
if (value === undefined) {
|
||||
@ -86,49 +89,68 @@ class Storage {
|
||||
}
|
||||
} catch (err: any) {
|
||||
// Safari in private mode doesn't allow localstorage
|
||||
} finally {
|
||||
if (this._listeners[storageKey]) {
|
||||
this._listeners[storageKey].forEach((listener) =>
|
||||
listener(oldValue, value)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subscribeStorage = new Storage();
|
||||
const storages: Record<string, StorageClass> = {};
|
||||
|
||||
export const LocalStorage =
|
||||
(
|
||||
storageKey?: string,
|
||||
property?: boolean,
|
||||
subscribe = true,
|
||||
storageType?: globalThis.Storage,
|
||||
propertyOptions?: PropertyDeclaration
|
||||
): any =>
|
||||
export const storage =
|
||||
(options: {
|
||||
key?: string;
|
||||
storage?: "localStorage" | "sessionStorage";
|
||||
subscribe?: boolean;
|
||||
state?: boolean;
|
||||
stateOptions?: InternalPropertyDeclaration;
|
||||
}): any =>
|
||||
(clsElement: ClassElement) => {
|
||||
const storage =
|
||||
subscribe && !storageType
|
||||
? subscribeStorage
|
||||
: new Storage(subscribe, storageType);
|
||||
const storageName = options.storage || "localStorage";
|
||||
|
||||
let storageInstance: StorageClass;
|
||||
if (storageName && storageName in storages) {
|
||||
storageInstance = storages[storageName];
|
||||
} else {
|
||||
storageInstance = new StorageClass(window[storageName]);
|
||||
storages[storageName] = storageInstance;
|
||||
}
|
||||
|
||||
const key = String(clsElement.key);
|
||||
storageKey = storageKey || String(clsElement.key);
|
||||
const storageKey = options.key || String(clsElement.key);
|
||||
const initVal = clsElement.initializer
|
||||
? clsElement.initializer()
|
||||
: undefined;
|
||||
|
||||
storage.addFromStorage(storageKey);
|
||||
storageInstance.addFromStorage(storageKey);
|
||||
|
||||
const subscribeChanges = (el: ReactiveElement): UnsubscribeFunc =>
|
||||
storage.subscribeChanges(storageKey!, (oldValue) => {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
});
|
||||
const subscribeChanges =
|
||||
options.subscribe !== false
|
||||
? (el: ReactiveElement): UnsubscribeFunc =>
|
||||
storageInstance.subscribeChanges(
|
||||
storageKey!,
|
||||
(oldValue, _newValue) => {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
}
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const getValue = (): any =>
|
||||
storage.hasKey(storageKey!) ? storage.getValue(storageKey!) : initVal;
|
||||
storageInstance.hasKey(storageKey!)
|
||||
? storageInstance.getValue(storageKey!)
|
||||
: initVal;
|
||||
|
||||
const setValue = (el: ReactiveElement, value: any) => {
|
||||
let oldValue: unknown | undefined;
|
||||
if (property) {
|
||||
if (options.state) {
|
||||
oldValue = getValue();
|
||||
}
|
||||
storage.setValue(storageKey!, value);
|
||||
if (property) {
|
||||
storageInstance.setValue(storageKey!, value);
|
||||
if (options.state) {
|
||||
el.requestUpdate(clsElement.key, oldValue);
|
||||
}
|
||||
};
|
||||
@ -148,22 +170,23 @@ export const LocalStorage =
|
||||
configurable: true,
|
||||
},
|
||||
finisher(cls: typeof ReactiveElement) {
|
||||
if (property && subscribe) {
|
||||
if (options.state && options.subscribe) {
|
||||
const connectedCallback = cls.prototype.connectedCallback;
|
||||
const disconnectedCallback = cls.prototype.disconnectedCallback;
|
||||
cls.prototype.connectedCallback = function () {
|
||||
connectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribeChanges(this);
|
||||
this[`__unbsubLocalStorage${key}`] = subscribeChanges?.(this);
|
||||
};
|
||||
cls.prototype.disconnectedCallback = function () {
|
||||
disconnectedCallback.call(this);
|
||||
this[`__unbsubLocalStorage${key}`]();
|
||||
this[`__unbsubLocalStorage${key}`]?.();
|
||||
this[`__unbsubLocalStorage${key}`] = undefined;
|
||||
};
|
||||
}
|
||||
if (property) {
|
||||
if (options.state) {
|
||||
cls.createProperty(clsElement.key, {
|
||||
noAccessor: true,
|
||||
...propertyOptions,
|
||||
...options.stateOptions,
|
||||
});
|
||||
}
|
||||
},
|
@ -1,4 +1,4 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, TemplateResult } from "lit";
|
||||
import { until } from "lit/directives/until";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
@ -20,6 +20,7 @@ export const computeAttributeValueDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
attribute: string,
|
||||
value?: any
|
||||
@ -59,14 +60,14 @@ export const computeAttributeValueDisplay = (
|
||||
if (isTimestamp(attributeValue)) {
|
||||
const date = new Date(attributeValue);
|
||||
if (checkValidDate(date)) {
|
||||
return formatDateTimeWithSeconds(date, locale);
|
||||
return formatDateTimeWithSeconds(date, locale, config);
|
||||
}
|
||||
}
|
||||
|
||||
// Value was not a timestamp, so only do date formatting
|
||||
const date = new Date(attributeValue);
|
||||
if (checkValidDate(date)) {
|
||||
return formatDate(date, locale);
|
||||
return formatDate(date, locale, config);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -92,6 +93,7 @@ export const computeAttributeValueDisplay = (
|
||||
localize,
|
||||
stateObj,
|
||||
locale,
|
||||
config,
|
||||
entities,
|
||||
attribute,
|
||||
item
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { HassConfig, HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
|
||||
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { FrontendLocaleData, TimeZone } from "../../data/translation";
|
||||
import {
|
||||
updateIsInstallingFromAttributes,
|
||||
UPDATE_SUPPORT_PROGRESS,
|
||||
@ -28,12 +28,14 @@ export const computeStateDisplaySingleEntity = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
state?: string
|
||||
): string =>
|
||||
computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
@ -44,6 +46,7 @@ export const computeStateDisplay = (
|
||||
localize: LocalizeFunc,
|
||||
stateObj: HassEntity,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entities: HomeAssistant["entities"],
|
||||
state?: string
|
||||
): string => {
|
||||
@ -54,6 +57,7 @@ export const computeStateDisplay = (
|
||||
return computeStateDisplayFromEntityAttributes(
|
||||
localize,
|
||||
locale,
|
||||
config,
|
||||
entity,
|
||||
stateObj.entity_id,
|
||||
stateObj.attributes,
|
||||
@ -64,6 +68,7 @@ export const computeStateDisplay = (
|
||||
export const computeStateDisplayFromEntityAttributes = (
|
||||
localize: LocalizeFunc,
|
||||
locale: FrontendLocaleData,
|
||||
config: HassConfig,
|
||||
entity: EntityRegistryDisplayEntry | undefined,
|
||||
entityId: string,
|
||||
attributes: any,
|
||||
@ -119,29 +124,40 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
|
||||
if (domain === "datetime") {
|
||||
const time = new Date(state);
|
||||
return formatDateTime(time, locale);
|
||||
return formatDateTime(time, locale, config);
|
||||
}
|
||||
|
||||
if (["date", "input_datetime", "time"].includes(domain)) {
|
||||
// If trying to display an explicit state, need to parse the explicit state to `Date` then format.
|
||||
// Attributes aren't available, we have to use `state`.
|
||||
|
||||
// These are timezone agnostic, so we should NOT use the system timezone here.
|
||||
try {
|
||||
const components = state.split(" ");
|
||||
if (components.length === 2) {
|
||||
// Date and time.
|
||||
return formatDateTime(new Date(components.join("T")), locale);
|
||||
return formatDateTime(
|
||||
new Date(components.join("T")),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
}
|
||||
if (components.length === 1) {
|
||||
if (state.includes("-")) {
|
||||
// Date only.
|
||||
return formatDate(new Date(`${state}T00:00`), locale);
|
||||
return formatDate(
|
||||
new Date(`${state}T00:00`),
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
}
|
||||
if (state.includes(":")) {
|
||||
// Time only.
|
||||
const now = new Date();
|
||||
return formatTime(
|
||||
new Date(`${now.toISOString().split("T")[0]}T${state}`),
|
||||
locale
|
||||
{ ...locale, time_zone: TimeZone.local },
|
||||
config
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -153,12 +169,6 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
}
|
||||
}
|
||||
|
||||
if (domain === "humidifier") {
|
||||
if (state === "on" && attributes.humidity) {
|
||||
return `${attributes.humidity} %`;
|
||||
}
|
||||
}
|
||||
|
||||
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
|
||||
if (
|
||||
domain === "counter" ||
|
||||
@ -175,11 +185,13 @@ export const computeStateDisplayFromEntityAttributes = (
|
||||
|
||||
// state is a timestamp
|
||||
if (
|
||||
["button", "input_button", "scene", "stt", "tts"].includes(domain) ||
|
||||
["button", "image", "input_button", "scene", "stt", "tts"].includes(
|
||||
domain
|
||||
) ||
|
||||
(domain === "sensor" && attributes.device_class === "timestamp")
|
||||
) {
|
||||
try {
|
||||
return formatDateTime(new Date(state), locale);
|
||||
return formatDateTime(new Date(state), locale, config);
|
||||
} catch (_err) {
|
||||
return state;
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
mdiCheckCircleOutline,
|
||||
mdiClock,
|
||||
mdiCloseCircleOutline,
|
||||
mdiCrosshairsQuestion,
|
||||
mdiFan,
|
||||
mdiFanOff,
|
||||
mdiGestureTapButton,
|
||||
@ -31,6 +32,7 @@ import {
|
||||
mdiPowerPlugOff,
|
||||
mdiRestart,
|
||||
mdiRobot,
|
||||
mdiRobotConfused,
|
||||
mdiRobotOff,
|
||||
mdiSpeaker,
|
||||
mdiSpeakerOff,
|
||||
@ -91,13 +93,19 @@ export const domainIconWithoutDefault = (
|
||||
return alarmPanelIcon(compareState);
|
||||
|
||||
case "automation":
|
||||
return compareState === "off" ? mdiRobotOff : mdiRobot;
|
||||
return compareState === "unavailable"
|
||||
? mdiRobotConfused
|
||||
: compareState === "off"
|
||||
? mdiRobotOff
|
||||
: mdiRobot;
|
||||
|
||||
case "binary_sensor":
|
||||
return binarySensorIcon(compareState, stateObj);
|
||||
|
||||
case "button":
|
||||
switch (stateObj?.attributes.device_class) {
|
||||
case "identify":
|
||||
return mdiCrosshairsQuestion;
|
||||
case "restart":
|
||||
return mdiRestart;
|
||||
case "update":
|
||||
|
@ -30,6 +30,7 @@ export const FIXED_DOMAIN_STATES = {
|
||||
lock: ["jammed", "locked", "locking", "unlocked", "unlocking"],
|
||||
media_player: ["idle", "off", "paused", "playing", "standby"],
|
||||
person: ["home", "not_home"],
|
||||
plant: ["ok", "problem"],
|
||||
remote: ["on", "off"],
|
||||
scene: [],
|
||||
schedule: ["on", "off"],
|
||||
@ -102,7 +103,15 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
frontend_stream_type: ["hls", "web_rtc"],
|
||||
},
|
||||
climate: {
|
||||
hvac_action: ["off", "idle", "heating", "cooling", "drying", "fan"],
|
||||
hvac_action: [
|
||||
"off",
|
||||
"idle",
|
||||
"preheating",
|
||||
"heating",
|
||||
"cooling",
|
||||
"drying",
|
||||
"fan",
|
||||
],
|
||||
},
|
||||
cover: {
|
||||
device_class: [
|
||||
@ -126,6 +135,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
|
||||
},
|
||||
humidifier: {
|
||||
device_class: ["humidifier", "dehumidifier"],
|
||||
action: ["off", "idle", "humidifying", "drying"],
|
||||
},
|
||||
media_player: {
|
||||
device_class: ["tv", "speaker", "receiver"],
|
||||
|
@ -110,3 +110,15 @@ export const stateColorProperties = (
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const stateColorBrightness = (stateObj: HassEntity): string => {
|
||||
if (
|
||||
stateObj.attributes.brightness &&
|
||||
computeDomain(stateObj.entity_id) !== "plant"
|
||||
) {
|
||||
// lowest brightness will be around 50% (that's pretty dark)
|
||||
const brightness = stateObj.attributes.brightness;
|
||||
return `brightness(${(brightness + 245) / 5}%)`;
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
@ -17,12 +17,13 @@ export const stripPrefixFromEntityName = (
|
||||
|
||||
if (lowerCasedEntityName.startsWith(lowerCasedPrefixWithSuffix)) {
|
||||
const newName = entityName.substring(lowerCasedPrefixWithSuffix.length);
|
||||
|
||||
// If first word already has an upper case letter (e.g. from brand name)
|
||||
// leave as-is, otherwise capitalize the first word.
|
||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||
? newName
|
||||
: newName[0].toUpperCase() + newName.slice(1);
|
||||
if (newName.length) {
|
||||
// If first word already has an upper case letter (e.g. from brand name)
|
||||
// leave as-is, otherwise capitalize the first word.
|
||||
return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
|
||||
? newName
|
||||
: newName[0].toUpperCase() + newName.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
19
src/common/translations/auto_case_noun.ts
Normal file
19
src/common/translations/auto_case_noun.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// In a few languages nouns are always capitalized. This helper
|
||||
// indicates if for a given language that is the case.
|
||||
|
||||
import { capitalizeFirstLetter } from "../string/capitalize-first-letter";
|
||||
|
||||
export const useCapitalizedNouns = (language: string): boolean => {
|
||||
switch (language) {
|
||||
case "de":
|
||||
case "lb":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const autoCaseNoun = (noun: string, language: string): string =>
|
||||
useCapitalizedNouns(language)
|
||||
? capitalizeFirstLetter(noun)
|
||||
: noun.toLocaleLowerCase(language);
|
@ -1,10 +1,12 @@
|
||||
import { addDays, startOfWeek } from "date-fns";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatDateWeekday } from "../datetime/format_date";
|
||||
|
||||
export const dayNames = memoizeOne((locale: FrontendLocaleData): string[] =>
|
||||
Array.from({ length: 7 }, (_, d) =>
|
||||
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale)
|
||||
)
|
||||
export const dayNames = memoizeOne(
|
||||
(locale: FrontendLocaleData, config: HassConfig): string[] =>
|
||||
Array.from({ length: 7 }, (_, d) =>
|
||||
formatDateWeekday(addDays(startOfWeek(new Date()), d), locale, config)
|
||||
)
|
||||
);
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { addMonths, startOfYear } from "date-fns";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { FrontendLocaleData } from "../../data/translation";
|
||||
import { formatDateMonth } from "../datetime/format_date";
|
||||
|
||||
export const monthNames = memoizeOne((locale: FrontendLocaleData): string[] =>
|
||||
Array.from({ length: 12 }, (_, m) =>
|
||||
formatDateMonth(addMonths(startOfYear(new Date()), m), locale)
|
||||
)
|
||||
export const monthNames = memoizeOne(
|
||||
(locale: FrontendLocaleData, config: HassConfig): string[] =>
|
||||
Array.from({ length: 12 }, (_, m) =>
|
||||
formatDateMonth(addMonths(startOfYear(new Date()), m), locale, config)
|
||||
)
|
||||
);
|
||||
|
@ -80,33 +80,89 @@ _adapters._date.override({
|
||||
format: function (time, fmt: keyof typeof FORMATS) {
|
||||
switch (fmt) {
|
||||
case "datetime":
|
||||
return formatDateTime(new Date(time), this.options.locale);
|
||||
return formatDateTime(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "datetimeseconds":
|
||||
return formatDateTimeWithSeconds(new Date(time), this.options.locale);
|
||||
return formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "millisecond":
|
||||
return formatTimeWithSeconds(new Date(time), this.options.locale);
|
||||
return formatTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "second":
|
||||
return formatTimeWithSeconds(new Date(time), this.options.locale);
|
||||
return formatTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "minute":
|
||||
return formatTime(new Date(time), this.options.locale);
|
||||
return formatTime(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "hour":
|
||||
return formatTime(new Date(time), this.options.locale);
|
||||
return formatTime(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "weekday":
|
||||
return formatDateWeekdayDay(new Date(time), this.options.locale);
|
||||
return formatDateWeekdayDay(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "date":
|
||||
return formatDate(new Date(time), this.options.locale);
|
||||
return formatDate(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "day":
|
||||
return formatDateShort(new Date(time), this.options.locale);
|
||||
return formatDateShort(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "week":
|
||||
return formatDate(new Date(time), this.options.locale);
|
||||
return formatDate(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "month":
|
||||
return formatDateMonth(new Date(time), this.options.locale);
|
||||
return formatDateMonth(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "monthyear":
|
||||
return formatDateMonthYear(new Date(time), this.options.locale);
|
||||
return formatDateMonthYear(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "quarter":
|
||||
return formatDate(new Date(time), this.options.locale);
|
||||
return formatDate(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
case "year":
|
||||
return formatDateYear(new Date(time), this.options.locale);
|
||||
return formatDateYear(
|
||||
new Date(time),
|
||||
this.options.locale,
|
||||
this.options.config
|
||||
);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
@ -30,6 +30,8 @@ class StateHistoryChartLine extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ attribute: false }) public startTime!: Date;
|
||||
|
||||
@property({ attribute: false }) public endTime!: Date;
|
||||
|
||||
@property({ type: Number }) public paddingYAxis = 0;
|
||||
@ -57,7 +59,12 @@ class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (!this.hasUpdated || changedProps.has("showNames")) {
|
||||
if (
|
||||
!this.hasUpdated ||
|
||||
changedProps.has("showNames") ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime")
|
||||
) {
|
||||
this._chartOptions = {
|
||||
parsing: false,
|
||||
animation: false,
|
||||
@ -71,8 +78,10 @@ class StateHistoryChartLine extends LitElement {
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
config: this.hass.config,
|
||||
},
|
||||
},
|
||||
suggestedMin: this.startTime,
|
||||
suggestedMax: this.endTime,
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
@ -145,6 +154,8 @@ class StateHistoryChartLine extends LitElement {
|
||||
}
|
||||
if (
|
||||
changedProps.has("data") ||
|
||||
changedProps.has("startTime") ||
|
||||
changedProps.has("endTime") ||
|
||||
this._chartTime <
|
||||
new Date(this.endTime.getTime() - MIN_TIME_BETWEEN_UPDATES)
|
||||
) {
|
||||
@ -375,6 +386,9 @@ class StateHistoryChartLine extends LitElement {
|
||||
lastNullDate = date;
|
||||
}
|
||||
});
|
||||
if (lastNullDate !== null) {
|
||||
pushData(lastNullDate, [null]);
|
||||
}
|
||||
}
|
||||
|
||||
// Add an entry for final values
|
||||
|
@ -98,6 +98,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
config: this.hass.config,
|
||||
},
|
||||
},
|
||||
suggestedMin: this.startTime,
|
||||
@ -181,8 +182,16 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
return [
|
||||
d.label || "",
|
||||
formatDateTimeWithSeconds(d.start, this.hass.locale),
|
||||
formatDateTimeWithSeconds(d.end, this.hass.locale),
|
||||
formatDateTimeWithSeconds(
|
||||
d.start,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formatDateTimeWithSeconds(
|
||||
d.end,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formattedDuration,
|
||||
];
|
||||
},
|
||||
|
@ -52,8 +52,12 @@ export class StateHistoryCharts extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public endTime?: Date;
|
||||
|
||||
@property({ attribute: false }) public startTime?: Date;
|
||||
|
||||
@property({ type: Boolean, attribute: "up-to-now" }) public upToNow = false;
|
||||
|
||||
@property() public hoursToShow?: number;
|
||||
|
||||
@property({ type: Boolean }) public showNames = true;
|
||||
|
||||
@property({ type: Boolean }) public isLoadingData = false;
|
||||
@ -95,13 +99,24 @@ export class StateHistoryCharts extends LitElement {
|
||||
this._computedEndTime =
|
||||
this.upToNow || !this.endTime || this.endTime > now ? now : this.endTime;
|
||||
|
||||
this._computedStartTime = new Date(
|
||||
this.historyData.timeline.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(minTime, new Date(stateInfo.data[0].last_changed).getTime()),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
if (this.startTime) {
|
||||
this._computedStartTime = this.startTime;
|
||||
} else if (this.hoursToShow) {
|
||||
this._computedStartTime = new Date(
|
||||
new Date().getTime() - 60 * 60 * this.hoursToShow * 1000
|
||||
);
|
||||
} else {
|
||||
this._computedStartTime = new Date(
|
||||
this.historyData.timeline.reduce(
|
||||
(minTime, stateInfo) =>
|
||||
Math.min(
|
||||
minTime,
|
||||
new Date(stateInfo.data[0].last_changed).getTime()
|
||||
),
|
||||
new Date().getTime()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const combinedItems = this.historyData.timeline.length
|
||||
? (this.virtualize
|
||||
@ -142,6 +157,7 @@ export class StateHistoryCharts extends LitElement {
|
||||
.data=${item.data}
|
||||
.identifier=${item.identifier}
|
||||
.showNames=${this.showNames}
|
||||
.startTime=${this._computedStartTime}
|
||||
.endTime=${this._computedEndTime}
|
||||
.paddingYAxis=${this._maxYWidth}
|
||||
.names=${this.names}
|
||||
|
@ -146,6 +146,7 @@ class StatisticsChart extends LitElement {
|
||||
adapters: {
|
||||
date: {
|
||||
locale: this.hass.locale,
|
||||
config: this.hass.config,
|
||||
},
|
||||
},
|
||||
ticks: {
|
||||
@ -165,7 +166,7 @@ class StatisticsChart extends LitElement {
|
||||
},
|
||||
},
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
beginAtZero: this.chartType === "bar",
|
||||
ticks: {
|
||||
maxTicksLimit: 7,
|
||||
},
|
||||
|
@ -349,6 +349,7 @@ export class HaDataTable extends LitElement {
|
||||
class="mdc-data-table__content scroller ha-scrollbar"
|
||||
@scroll=${this._saveScrollPos}
|
||||
.items=${this._items}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderRow}
|
||||
></lit-virtualizer>
|
||||
`}
|
||||
@ -357,6 +358,8 @@ export class HaDataTable extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _keyFunction = (row: DataTableRowData) => row[this.id] || row;
|
||||
|
||||
private _renderRow = (row: DataTableRowData, index: number) => {
|
||||
// not sure how this happens...
|
||||
if (!row) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Remote, wrap } from "comlink";
|
||||
import type { Api } from "./sort_filter_worker";
|
||||
import type { Api } from "./sort-filter-worker";
|
||||
|
||||
type FilterDataType = Api["filterData"];
|
||||
type FilterDataParamTypes = Parameters<FilterDataType>;
|
||||
@ -9,27 +9,28 @@ type SortDataParamTypes = Parameters<SortDataType>;
|
||||
|
||||
let worker: Remote<Api> | undefined;
|
||||
|
||||
const getWorker = () => {
|
||||
if (!worker) {
|
||||
worker = wrap(
|
||||
new Worker(
|
||||
/* webpackChunkName: "sort-filter-worker" */
|
||||
new URL("./sort-filter-worker", import.meta.url)
|
||||
)
|
||||
);
|
||||
}
|
||||
return worker;
|
||||
};
|
||||
|
||||
export const filterData = (
|
||||
data: FilterDataParamTypes[0],
|
||||
columns: FilterDataParamTypes[1],
|
||||
filter: FilterDataParamTypes[2]
|
||||
): Promise<ReturnType<FilterDataType>> => {
|
||||
if (!worker) {
|
||||
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
|
||||
}
|
||||
|
||||
return worker.filterData(data, columns, filter);
|
||||
};
|
||||
|
||||
): Promise<ReturnType<FilterDataType>> =>
|
||||
getWorker().filterData(data, columns, filter);
|
||||
export const sortData = (
|
||||
data: SortDataParamTypes[0],
|
||||
columns: SortDataParamTypes[1],
|
||||
direction: SortDataParamTypes[2],
|
||||
sortColumn: SortDataParamTypes[3]
|
||||
): Promise<ReturnType<SortDataType>> => {
|
||||
if (!worker) {
|
||||
worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
|
||||
}
|
||||
|
||||
return worker.sortData(data, columns, direction, sortColumn);
|
||||
};
|
||||
): Promise<ReturnType<SortDataType>> =>
|
||||
getWorker().sortData(data, columns, direction, sortColumn);
|
||||
|
@ -26,6 +26,10 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import {
|
||||
fuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
|
||||
interface Device {
|
||||
name: string;
|
||||
@ -33,6 +37,8 @@ interface Device {
|
||||
id: string;
|
||||
}
|
||||
|
||||
type ScorableDevice = ScorableTextItem & Device;
|
||||
|
||||
export type HaDevicePickerDeviceFilterFunc = (
|
||||
device: DeviceRegistryEntry
|
||||
) => boolean;
|
||||
@ -119,13 +125,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
excludeDevices: this["excludeDevices"]
|
||||
): Device[] => {
|
||||
): ScorableDevice[] => {
|
||||
if (!devices.length) {
|
||||
return [
|
||||
{
|
||||
id: "no_devices",
|
||||
area: "",
|
||||
name: this.hass.localize("ui.components.device-picker.no_devices"),
|
||||
strings: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -235,6 +242,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
device.area_id && areaLookup[device.area_id]
|
||||
? areaLookup[device.area_id].name
|
||||
: this.hass.localize("ui.components.device-picker.no_area"),
|
||||
strings: [device.name || ""],
|
||||
}));
|
||||
if (!outputDevices.length) {
|
||||
return [
|
||||
@ -242,6 +250,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
id: "no_devices",
|
||||
area: "",
|
||||
name: this.hass.localize("ui.components.device-picker.no_match"),
|
||||
strings: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -284,7 +293,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
(this._init && changedProps.has("_opened") && this._opened)
|
||||
) {
|
||||
this._init = true;
|
||||
(this.comboBox as any).items = this._getDevices(
|
||||
const devices = this._getDevices(
|
||||
this.devices!,
|
||||
this.areas!,
|
||||
this.entities!,
|
||||
@ -295,6 +304,8 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
this.entityFilter,
|
||||
this.excludeDevices
|
||||
);
|
||||
this.comboBox.items = devices;
|
||||
this.comboBox.filteredItems = devices;
|
||||
}
|
||||
}
|
||||
|
||||
@ -314,6 +325,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
item-label-path="name"
|
||||
@opened-changed=${this._openedChanged}
|
||||
@value-changed=${this._deviceChanged}
|
||||
@filter-changed=${this._filterChanged}
|
||||
></ha-combo-box>
|
||||
`;
|
||||
}
|
||||
@ -322,6 +334,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
|
||||
return this.value || "";
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
target.filteredItems = filterString.length
|
||||
? fuzzyFilterSort<ScorableDevice>(filterString, target.items || [])
|
||||
: target.items;
|
||||
}
|
||||
|
||||
private _deviceChanged(ev: ValueChangedEvent<string>) {
|
||||
ev.stopPropagation();
|
||||
let newValue = ev.detail.value;
|
||||
|
@ -7,15 +7,19 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import {
|
||||
fuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../../common/string/filter/sequence-matching";
|
||||
import { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import "../ha-combo-box";
|
||||
import type { HaComboBox } from "../ha-combo-box";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-svg-icon";
|
||||
import "./state-badge";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
|
||||
interface HassEntityWithCachedName extends HassEntity {
|
||||
interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
|
||||
friendly_name: string;
|
||||
}
|
||||
|
||||
@ -159,6 +163,7 @@ export class HaEntityPicker extends LitElement {
|
||||
),
|
||||
icon: "mdi:magnify",
|
||||
},
|
||||
strings: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -169,10 +174,14 @@ export class HaEntityPicker extends LitElement {
|
||||
);
|
||||
|
||||
return entityIds
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.map((key) => {
|
||||
const friendly_name = computeStateName(hass!.states[key]) || key;
|
||||
return {
|
||||
...hass!.states[key],
|
||||
friendly_name,
|
||||
strings: [key, friendly_name],
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
@ -201,10 +210,14 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
states = entityIds
|
||||
.map((key) => ({
|
||||
...hass!.states[key],
|
||||
friendly_name: computeStateName(hass!.states[key]) || key,
|
||||
}))
|
||||
.map((key) => {
|
||||
const friendly_name = computeStateName(hass!.states[key]) || key;
|
||||
return {
|
||||
...hass!.states[key],
|
||||
friendly_name,
|
||||
strings: [key, friendly_name],
|
||||
};
|
||||
})
|
||||
.sort((entityA, entityB) =>
|
||||
caseInsensitiveStringCompare(
|
||||
entityA.friendly_name,
|
||||
@ -260,6 +273,7 @@ export class HaEntityPicker extends LitElement {
|
||||
),
|
||||
icon: "mdi:magnify",
|
||||
},
|
||||
strings: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -293,7 +307,7 @@ export class HaEntityPicker extends LitElement {
|
||||
this.excludeEntities
|
||||
);
|
||||
if (this._initedStates) {
|
||||
(this.comboBox as any).filteredItems = this._states;
|
||||
this.comboBox.filteredItems = this._states;
|
||||
}
|
||||
this._initedStates = true;
|
||||
}
|
||||
@ -340,12 +354,11 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _filterChanged(ev: CustomEvent): void {
|
||||
const target = ev.target as HaComboBox;
|
||||
const filterString = ev.detail.value.toLowerCase();
|
||||
(this.comboBox as any).filteredItems = this._states.filter(
|
||||
(entityState) =>
|
||||
entityState.entity_id.toLowerCase().includes(filterString) ||
|
||||
computeStateName(entityState).toLowerCase().includes(filterString)
|
||||
);
|
||||
target.filteredItems = filterString.length
|
||||
? fuzzyFilterSort<HassEntityWithCachedName>(filterString, this._states)
|
||||
: this._states;
|
||||
}
|
||||
|
||||
private _setValue(value: string) {
|
||||
|
@ -62,6 +62,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
this.hass.localize,
|
||||
state,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
key
|
||||
)
|
||||
@ -69,6 +70,7 @@ class HaEntityStatePicker extends LitElement {
|
||||
this.hass.localize,
|
||||
state,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
this.attribute,
|
||||
key
|
||||
|
@ -192,6 +192,7 @@ export class HaStateLabelBadge extends LitElement {
|
||||
this.hass!.localize,
|
||||
entityState,
|
||||
this.hass!.locale,
|
||||
this.hass!.config,
|
||||
this.hass!.entities
|
||||
);
|
||||
}
|
||||
|
@ -13,7 +13,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { stateColorCss } from "../../common/entity/state_color";
|
||||
import {
|
||||
stateColorCss,
|
||||
stateColorBrightness,
|
||||
} from "../../common/entity/state_color";
|
||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
||||
import { HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
@ -153,8 +156,7 @@ export class StateBadge extends LitElement {
|
||||
// eslint-disable-next-line
|
||||
console.warn(errorMessage);
|
||||
}
|
||||
// lowest brightness will be around 50% (that's pretty dark)
|
||||
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
|
||||
iconStyle.filter = stateColorBrightness(stateObj);
|
||||
}
|
||||
if (stateObj.attributes.hvac_action) {
|
||||
const hvacAction = stateObj.attributes.hvac_action;
|
||||
|
@ -64,7 +64,11 @@ class HaAbsoluteTime extends ReactiveElement {
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this.hass.localize("ui.components.absolute_time.never");
|
||||
} else {
|
||||
this.innerHTML = absoluteTime(new Date(this.datetime), this.hass.locale);
|
||||
this.innerHTML = absoluteTime(
|
||||
new Date(this.datetime),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import {
|
||||
fuzzyFilterSort,
|
||||
ScorableTextItem,
|
||||
} from "../common/string/filter/sequence-matching";
|
||||
import {
|
||||
AreaRegistryEntry,
|
||||
createAreaRegistryEntry,
|
||||
@ -28,6 +32,8 @@ import type { HaComboBox } from "./ha-combo-box";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
type ScorableAreaRegistryEntry = ScorableTextItem & AreaRegistryEntry;
|
||||
|
||||
const rowRenderer: ComboBoxLitRenderer<AreaRegistryEntry> = (
|
||||
item
|
||||
) => html`<mwc-list-item
|
||||
@ -306,9 +312,12 @@ export class HaAreaPicker extends LitElement {
|
||||
this.entityFilter,
|
||||
this.noAdd,
|
||||
this.excludeAreas
|
||||
);
|
||||
(this.comboBox as any).items = areas;
|
||||
(this.comboBox as any).filteredItems = areas;
|
||||
).map((area) => ({
|
||||
...area,
|
||||
strings: [area.area_id, ...area.aliases, area.name],
|
||||
}));
|
||||
this.comboBox.items = areas;
|
||||
this.comboBox.filteredItems = areas;
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,8 +354,9 @@ export class HaAreaPicker extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredItems = this.comboBox.items?.filter((item) =>
|
||||
item.name.toLowerCase().includes(filter!.toLowerCase())
|
||||
const filteredItems = fuzzyFilterSort<ScorableAreaRegistryEntry>(
|
||||
filter,
|
||||
this.comboBox?.items || []
|
||||
);
|
||||
if (!this.noAdd && filteredItems?.length === 0) {
|
||||
this._suggestion = filter;
|
||||
@ -409,7 +419,7 @@ export class HaAreaPicker extends LitElement {
|
||||
name,
|
||||
});
|
||||
const areas = [...Object.values(this.hass.areas), area];
|
||||
(this.comboBox as any).filteredItems = this._getAreas(
|
||||
this.comboBox.filteredItems = this._getAreas(
|
||||
areas,
|
||||
Object.values(this.hass.devices)!,
|
||||
Object.values(this.hass.entities)!,
|
||||
|
@ -62,6 +62,7 @@ class HaAttributes extends LitElement {
|
||||
this.hass.localize,
|
||||
this.stateObj!,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
attribute
|
||||
)}
|
||||
|
@ -28,6 +28,7 @@ class HaClimateState extends LitElement {
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"preset_mode"
|
||||
)}`
|
||||
@ -136,6 +137,7 @@ class HaClimateState extends LitElement {
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
@ -144,6 +146,7 @@ class HaClimateState extends LitElement {
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"hvac_action"
|
||||
)} (${stateString})`
|
||||
|
618
src/components/ha-control-circular-slider.ts
Normal file
618
src/components/ha-control-circular-slider.ts
Normal file
@ -0,0 +1,618 @@
|
||||
import {
|
||||
DIRECTION_ALL,
|
||||
Manager,
|
||||
Pan,
|
||||
Tap,
|
||||
TouchMouseInput,
|
||||
} from "@egjs/hammerjs";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
svg,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { clamp } from "../common/number/clamp";
|
||||
import { arc } from "../resources/svg-arc";
|
||||
|
||||
const MAX_ANGLE = 270;
|
||||
const ROTATE_ANGLE = 360 - MAX_ANGLE / 2 - 90;
|
||||
const RADIUS = 145;
|
||||
|
||||
function xy2polar(x: number, y: number) {
|
||||
const r = Math.sqrt(x * x + y * y);
|
||||
const phi = Math.atan2(y, x);
|
||||
return [r, phi];
|
||||
}
|
||||
|
||||
function rad2deg(rad: number) {
|
||||
return (rad / (2 * Math.PI)) * 360;
|
||||
}
|
||||
|
||||
type ActiveSlider = "low" | "high" | "value";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"value-changing": { value: unknown };
|
||||
"low-changing": { value: unknown };
|
||||
"low-changed": { value: unknown };
|
||||
"high-changing": { value: unknown };
|
||||
"high-changed": { value: unknown };
|
||||
}
|
||||
}
|
||||
|
||||
const A11Y_KEY_CODES = new Set([
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
]);
|
||||
|
||||
@customElement("ha-control-circular-slider")
|
||||
export class HaControlCircularSlider extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public disabled = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public dual?: boolean;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public inverted?: boolean;
|
||||
|
||||
@property({ type: String })
|
||||
public label?: string;
|
||||
|
||||
@property({ type: String, attribute: "low-label" })
|
||||
public lowLabel?: string;
|
||||
|
||||
@property({ type: String, attribute: "high-label" })
|
||||
public highLabel?: string;
|
||||
|
||||
@property({ type: Number })
|
||||
public value?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public low?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public high?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public current?: number;
|
||||
|
||||
@property({ type: Number })
|
||||
public step = 1;
|
||||
|
||||
@property({ type: Number })
|
||||
public min = 0;
|
||||
|
||||
@property({ type: Number })
|
||||
public max = 100;
|
||||
|
||||
@state()
|
||||
public _localValue?: number = this.value;
|
||||
|
||||
@state()
|
||||
public _localLow?: number = this.low;
|
||||
|
||||
@state()
|
||||
public _localHigh?: number = this.high;
|
||||
|
||||
@state()
|
||||
public _activeSlider?: ActiveSlider;
|
||||
|
||||
@state()
|
||||
public _lastSlider?: ActiveSlider;
|
||||
|
||||
private _valueToPercentage(value: number) {
|
||||
return (
|
||||
(clamp(value, this.min, this.max) - this.min) / (this.max - this.min)
|
||||
);
|
||||
}
|
||||
|
||||
private _percentageToValue(value: number) {
|
||||
return (this.max - this.min) * value + this.min;
|
||||
}
|
||||
|
||||
private _steppedValue(value: number) {
|
||||
return Math.round(value / this.step) * this.step;
|
||||
}
|
||||
|
||||
private _boundedValue(value: number) {
|
||||
const min =
|
||||
this._activeSlider === "high"
|
||||
? Math.min(this._localLow ?? this.max)
|
||||
: this.min;
|
||||
const max =
|
||||
this._activeSlider === "low"
|
||||
? Math.max(this._localHigh ?? this.min)
|
||||
: this.max;
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (!this._activeSlider) {
|
||||
if (changedProps.has("value")) {
|
||||
this._localValue = this.value;
|
||||
}
|
||||
if (changedProps.has("low")) {
|
||||
this._localLow = this.low;
|
||||
}
|
||||
if (changedProps.has("high")) {
|
||||
this._localHigh = this.high;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this._setupListeners();
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _mc?: HammerManager;
|
||||
|
||||
private _getPercentageFromEvent = (e: HammerInput) => {
|
||||
const bound = this._slider.getBoundingClientRect();
|
||||
const x = (2 * (e.center.x - bound.left - bound.width / 2)) / bound.width;
|
||||
const y = (2 * (e.center.y - bound.top - bound.height / 2)) / bound.height;
|
||||
|
||||
const [, phi] = xy2polar(x, y);
|
||||
|
||||
const offset = (360 - MAX_ANGLE) / 2;
|
||||
|
||||
const angle = ((rad2deg(phi) + offset - ROTATE_ANGLE + 360) % 360) - offset;
|
||||
|
||||
return Math.max(Math.min(angle / MAX_ANGLE, 1), 0);
|
||||
};
|
||||
|
||||
@query("#slider")
|
||||
private _slider;
|
||||
|
||||
@query("#interaction")
|
||||
private _interaction;
|
||||
|
||||
private _findActiveSlider(value: number): ActiveSlider {
|
||||
if (!this.dual) return "value";
|
||||
const low = Math.max(this._localLow ?? this.min, this.min);
|
||||
const high = Math.min(this._localHigh ?? this.max, this.max);
|
||||
if (low >= value) {
|
||||
return "low";
|
||||
}
|
||||
if (high <= value) {
|
||||
return "high";
|
||||
}
|
||||
const lowDistance = Math.abs(value - low);
|
||||
const highDistance = Math.abs(value - high);
|
||||
return lowDistance <= highDistance ? "low" : "high";
|
||||
}
|
||||
|
||||
private _setActiveValue(value: number) {
|
||||
switch (this._activeSlider) {
|
||||
case "high":
|
||||
this._localHigh = value;
|
||||
break;
|
||||
case "low":
|
||||
this._localLow = value;
|
||||
break;
|
||||
case "value":
|
||||
this._localValue = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private _getActiveValue(): number | undefined {
|
||||
switch (this._activeSlider) {
|
||||
case "high":
|
||||
return this._localHigh;
|
||||
case "low":
|
||||
return this._localLow;
|
||||
case "value":
|
||||
return this._localValue;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
_setupListeners() {
|
||||
if (this._interaction && !this._mc) {
|
||||
this._mc = new Manager(this._interaction, {
|
||||
inputClass: TouchMouseInput,
|
||||
});
|
||||
this._mc.add(
|
||||
new Pan({
|
||||
direction: DIRECTION_ALL,
|
||||
enable: true,
|
||||
threshold: 0,
|
||||
})
|
||||
);
|
||||
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
|
||||
this._mc.on("pan", (e) => {
|
||||
e.srcEvent.stopPropagation();
|
||||
e.srcEvent.preventDefault();
|
||||
});
|
||||
this._mc.on("panstart", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
this._activeSlider = this._findActiveSlider(raw);
|
||||
this._lastSlider = this._activeSlider;
|
||||
this.shadowRoot?.getElementById("#slider")?.focus();
|
||||
});
|
||||
this._mc.on("pancancel", () => {
|
||||
if (this.disabled) return;
|
||||
this._activeSlider = undefined;
|
||||
});
|
||||
this._mc.on("panmove", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
const bounded = this._boundedValue(raw);
|
||||
this._setActiveValue(bounded);
|
||||
const stepped = this._steppedValue(bounded);
|
||||
if (this._activeSlider) {
|
||||
fireEvent(this, `${this._activeSlider}-changing`, { value: stepped });
|
||||
}
|
||||
});
|
||||
this._mc.on("panend", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
const bounded = this._boundedValue(raw);
|
||||
const stepped = this._steppedValue(bounded);
|
||||
this._setActiveValue(stepped);
|
||||
if (this._activeSlider) {
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: undefined,
|
||||
});
|
||||
fireEvent(this, `${this._activeSlider}-changed`, { value: stepped });
|
||||
}
|
||||
this._activeSlider = undefined;
|
||||
});
|
||||
this._mc.on("singletap", (e) => {
|
||||
if (this.disabled) return;
|
||||
const percentage = this._getPercentageFromEvent(e);
|
||||
const raw = this._percentageToValue(percentage);
|
||||
this._activeSlider = this._findActiveSlider(raw);
|
||||
const bounded = this._boundedValue(raw);
|
||||
const stepped = this._steppedValue(bounded);
|
||||
this._setActiveValue(stepped);
|
||||
if (this._activeSlider) {
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: undefined,
|
||||
});
|
||||
fireEvent(this, `${this._activeSlider}-changed`, { value: stepped });
|
||||
}
|
||||
this._lastSlider = this._activeSlider;
|
||||
this.shadowRoot?.getElementById("#slider")?.focus();
|
||||
this._activeSlider = undefined;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
return Math.max(this.step, (this.max - this.min) / 10);
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
if (this._lastSlider) {
|
||||
this.shadowRoot?.getElementById(this._lastSlider)?.focus();
|
||||
}
|
||||
this._activeSlider =
|
||||
this._lastSlider ?? ((e.currentTarget as any).id as ActiveSlider);
|
||||
this._lastSlider = undefined;
|
||||
|
||||
const value = this._getActiveValue();
|
||||
|
||||
switch (e.code) {
|
||||
case "ArrowRight":
|
||||
case "ArrowUp":
|
||||
this._setActiveValue(
|
||||
this._boundedValue((value ?? this.min) + this.step)
|
||||
);
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
case "ArrowDown":
|
||||
this._setActiveValue(
|
||||
this._boundedValue((value ?? this.min) - this.step)
|
||||
);
|
||||
break;
|
||||
case "PageUp":
|
||||
this._setActiveValue(
|
||||
this._steppedValue(
|
||||
this._boundedValue((value ?? this.min) + this._tenPercentStep)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "PageDown":
|
||||
this._setActiveValue(
|
||||
this._steppedValue(
|
||||
this._boundedValue((value ?? this.min) - this._tenPercentStep)
|
||||
)
|
||||
);
|
||||
break;
|
||||
case "Home":
|
||||
this._setActiveValue(this._boundedValue(this.min));
|
||||
break;
|
||||
case "End":
|
||||
this._setActiveValue(this._boundedValue(this.max));
|
||||
break;
|
||||
}
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: this._getActiveValue(),
|
||||
});
|
||||
this._activeSlider = undefined;
|
||||
}
|
||||
|
||||
_handleKeyUp(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
this._activeSlider = (e.currentTarget as any).id as ActiveSlider;
|
||||
e.preventDefault();
|
||||
fireEvent(this, `${this._activeSlider}-changing`, {
|
||||
value: undefined,
|
||||
});
|
||||
fireEvent(this, `${this._activeSlider}-changed`, {
|
||||
value: this._getActiveValue(),
|
||||
});
|
||||
this._activeSlider = undefined;
|
||||
}
|
||||
|
||||
destroyListeners() {
|
||||
if (this._mc) {
|
||||
this._mc.destroy();
|
||||
this._mc = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _strokeDashArc(
|
||||
percentage: number,
|
||||
inverted?: boolean
|
||||
): [string, string] {
|
||||
const maxRatio = MAX_ANGLE / 360;
|
||||
const f = RADIUS * 2 * Math.PI;
|
||||
if (inverted) {
|
||||
const arcLength = (1 - percentage) * f * maxRatio;
|
||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
||||
const strokeDashOffset = `${arcLength + f * (1 - maxRatio)}`;
|
||||
return [strokeDasharray, strokeDashOffset];
|
||||
}
|
||||
const arcLength = percentage * f * maxRatio;
|
||||
const strokeDasharray = `${arcLength} ${f - arcLength}`;
|
||||
const strokeDashOffset = "0";
|
||||
return [strokeDasharray, strokeDashOffset];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const trackPath = arc({ x: 0, y: 0, start: 0, end: MAX_ANGLE, r: RADIUS });
|
||||
|
||||
const lowValue = this.dual ? this._localLow : this._localValue;
|
||||
const highValue = this._localHigh;
|
||||
const lowPercentage = this._valueToPercentage(lowValue ?? this.min);
|
||||
const highPercentage = this._valueToPercentage(highValue ?? this.max);
|
||||
|
||||
const [lowStrokeDasharray, lowStrokeDashOffset] = this._strokeDashArc(
|
||||
lowPercentage,
|
||||
this.inverted
|
||||
);
|
||||
|
||||
const [highStrokeDasharray, highStrokeDashOffset] = this._strokeDashArc(
|
||||
highPercentage,
|
||||
true
|
||||
);
|
||||
|
||||
const currentPercentage = this._valueToPercentage(this.current ?? 0);
|
||||
const currentAngle = currentPercentage * MAX_ANGLE;
|
||||
|
||||
return html`
|
||||
<svg
|
||||
id="slider"
|
||||
viewBox="0 0 320 320"
|
||||
overflow="visible"
|
||||
class=${classMap({
|
||||
pressed: Boolean(this._activeSlider),
|
||||
})}
|
||||
@keydown=${this._handleKeyDown}
|
||||
tabindex=${this._lastSlider ? "0" : "-1"}
|
||||
>
|
||||
<g
|
||||
id="container"
|
||||
transform="translate(160 160) rotate(${ROTATE_ANGLE})"
|
||||
>
|
||||
<g id="interaction">
|
||||
<path d=${trackPath} />
|
||||
</g>
|
||||
<g id="display">
|
||||
<path class="background" d=${trackPath} />
|
||||
${lowValue != null
|
||||
? svg`
|
||||
<circle
|
||||
.id=${this.dual ? "low" : "value"}
|
||||
class="track"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r=${RADIUS}
|
||||
stroke-dasharray=${lowStrokeDasharray}
|
||||
stroke-dashoffset=${lowStrokeDashOffset}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
lowValue != null ? this._steppedValue(lowValue) : undefined
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.lowLabel ?? this.label)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${this.dual && highValue != null
|
||||
? svg`
|
||||
<circle
|
||||
id="high"
|
||||
class="track"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r=${RADIUS}
|
||||
stroke-dasharray=${highStrokeDasharray}
|
||||
stroke-dashoffset=${highStrokeDashOffset}
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-valuenow=${
|
||||
highValue != null
|
||||
? this._steppedValue(highValue)
|
||||
: undefined
|
||||
}
|
||||
aria-disabled=${this.disabled}
|
||||
aria-label=${ifDefined(this.highLabel)}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${this.current != null
|
||||
? svg`
|
||||
<g
|
||||
style=${styleMap({ "--current-angle": `${currentAngle}deg` })}
|
||||
class="current"
|
||||
>
|
||||
<line
|
||||
x1=${RADIUS - 12}
|
||||
y1="0"
|
||||
x2=${RADIUS - 15}
|
||||
y2="0"
|
||||
stroke-width="4"
|
||||
/>
|
||||
<line
|
||||
x1=${RADIUS - 15}
|
||||
y1="0"
|
||||
x2=${RADIUS - 20}
|
||||
y2="0"
|
||||
stroke-linecap="round"
|
||||
stroke-width="4"
|
||||
/>
|
||||
</g>
|
||||
`
|
||||
: nothing}
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
--control-circular-slider-color: var(--primary-color);
|
||||
--control-circular-slider-background: #8b97a3;
|
||||
--control-circular-slider-background-opacity: 0.3;
|
||||
--control-circular-slider-low-color: var(
|
||||
--control-circular-slider-color
|
||||
);
|
||||
--control-circular-slider-high-color: var(
|
||||
--control-circular-slider-color
|
||||
);
|
||||
}
|
||||
svg {
|
||||
width: 320px;
|
||||
display: block;
|
||||
}
|
||||
#slider {
|
||||
outline: none;
|
||||
}
|
||||
#interaction {
|
||||
display: flex;
|
||||
fill: none;
|
||||
stroke: transparent;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 48px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#display {
|
||||
pointer-events: none;
|
||||
}
|
||||
:host([disabled]) #interaction {
|
||||
cursor: initial;
|
||||
}
|
||||
|
||||
.background {
|
||||
fill: none;
|
||||
stroke: var(--control-circular-slider-background);
|
||||
opacity: var(--control-circular-slider-background-opacity);
|
||||
transition: stroke 180ms ease-in-out, opacity 180ms ease-in-out;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 24px;
|
||||
}
|
||||
|
||||
.track {
|
||||
outline: none;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 24px;
|
||||
transition: stroke-width 300ms ease-in-out,
|
||||
stroke-dasharray 300ms ease-in-out,
|
||||
stroke-dashoffset 300ms ease-in-out, stroke 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
}
|
||||
|
||||
.track:focus-visible {
|
||||
stroke-width: 28px;
|
||||
}
|
||||
|
||||
.pressed .track {
|
||||
transition: stroke-width 300ms ease-in-out;
|
||||
}
|
||||
|
||||
.current {
|
||||
stroke: var(--primary-text-color);
|
||||
transform: rotate(var(--current-angle, 0));
|
||||
transition: transform 300ms ease-in-out;
|
||||
}
|
||||
|
||||
#value {
|
||||
stroke: var(--control-circular-slider-color);
|
||||
}
|
||||
|
||||
#low {
|
||||
stroke: var(--control-circular-slider-low-color);
|
||||
}
|
||||
|
||||
#high {
|
||||
stroke: var(--control-circular-slider-high-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-control-circular-slider": HaControlCircularSlider;
|
||||
}
|
||||
}
|
@ -176,7 +176,7 @@ export class HaControlSlider extends LitElement {
|
||||
this._mc = undefined;
|
||||
}
|
||||
this.removeEventListener("keydown", this._handleKeyDown);
|
||||
this.removeEventListener("keyup", this._handleKeyDown);
|
||||
this.removeEventListener("keyup", this._handleKeyUp);
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import { HassConfig } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatDateNumeric } from "../common/datetime/format_date";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import { formatDateNumeric } from "../common/datetime/format_date";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { TimeZone } from "../data/translation";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
@ -59,7 +61,11 @@ export class HaDateInput extends LitElement {
|
||||
.value=${this.value
|
||||
? formatDateNumeric(
|
||||
new Date(`${this.value.split("T")[0]}T00:00:00`),
|
||||
this.locale
|
||||
{
|
||||
...this.locale,
|
||||
time_zone: TimeZone.local,
|
||||
},
|
||||
{} as HassConfig
|
||||
)
|
||||
: ""}
|
||||
.required=${this.required}
|
||||
|
@ -3,6 +3,13 @@ import "@material/mwc-list/mwc-list";
|
||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
||||
import "@material/mwc-list/mwc-list-item";
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import {
|
||||
addDays,
|
||||
endOfDay,
|
||||
endOfWeek,
|
||||
startOfDay,
|
||||
startOfWeek,
|
||||
} from "date-fns";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
@ -12,10 +19,11 @@ import {
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { formatDateTime } from "../common/datetime/format_date_time";
|
||||
import { formatDate } from "../common/datetime/format_date";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { calcDate } from "../common/datetime/calc_date";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import { formatDate } from "../common/datetime/format_date";
|
||||
import { formatDateTime } from "../common/datetime/format_date_time";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import { HomeAssistant } from "../types";
|
||||
import "./date-range-picker";
|
||||
@ -34,7 +42,7 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@property() public endDate!: Date;
|
||||
|
||||
@property() public ranges?: DateRangePickerRanges;
|
||||
@property() public ranges?: DateRangePickerRanges | false;
|
||||
|
||||
@property() public autoApply = false;
|
||||
|
||||
@ -46,6 +54,70 @@ export class HaDateRangePicker extends LitElement {
|
||||
|
||||
@property({ type: String }) private _rtlDirection = "ltr";
|
||||
|
||||
protected willUpdate() {
|
||||
if (!this.hasUpdated && this.ranges === undefined) {
|
||||
const today = new Date();
|
||||
const weekStartsOn = firstWeekdayIndex(this.hass.locale);
|
||||
const weekStart = calcDate(
|
||||
today,
|
||||
startOfWeek,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
);
|
||||
const weekEnd = calcDate(
|
||||
today,
|
||||
endOfWeek,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
);
|
||||
|
||||
this.ranges = {
|
||||
[this.hass.localize("ui.components.date-range-picker.ranges.today")]: [
|
||||
calcDate(today, startOfDay, this.hass.locale, this.hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
calcDate(today, endOfDay, this.hass.locale, this.hass.config, {
|
||||
weekStartsOn,
|
||||
}),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.yesterday"
|
||||
)]: [
|
||||
calcDate(
|
||||
addDays(today, -1),
|
||||
startOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
),
|
||||
calcDate(
|
||||
addDays(today, -1),
|
||||
endOfDay,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
{
|
||||
weekStartsOn,
|
||||
}
|
||||
),
|
||||
],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.this_week"
|
||||
)]: [weekStart, weekEnd],
|
||||
[this.hass.localize(
|
||||
"ui.components.date-range-picker.ranges.last_week"
|
||||
)]: [addDays(weekStart, -7), addDays(weekEnd, -7)],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
if (changedProps.has("hass")) {
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
@ -65,15 +137,19 @@ export class HaDateRangePicker extends LitElement {
|
||||
twentyfour-hours=${this._hour24format}
|
||||
start-date=${this.startDate}
|
||||
end-date=${this.endDate}
|
||||
?ranges=${this.ranges !== undefined}
|
||||
?ranges=${this.ranges !== false}
|
||||
first-day=${firstWeekdayIndex(this.hass.locale)}
|
||||
>
|
||||
<div slot="input" class="date-range-inputs">
|
||||
<ha-svg-icon .path=${mdiCalendar}></ha-svg-icon>
|
||||
<ha-textfield
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(this.startDate, this.hass.locale)
|
||||
: formatDate(this.startDate, this.hass.locale)}
|
||||
? formatDateTime(
|
||||
this.startDate,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)
|
||||
: formatDate(this.startDate, this.hass.locale, this.hass.config)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.start_date"
|
||||
)}
|
||||
@ -83,8 +159,8 @@ export class HaDateRangePicker extends LitElement {
|
||||
></ha-textfield>
|
||||
<ha-textfield
|
||||
.value=${this.timePicker
|
||||
? formatDateTime(this.endDate, this.hass.locale)
|
||||
: formatDate(this.endDate, this.hass.locale)}
|
||||
? formatDateTime(this.endDate, this.hass.locale, this.hass.config)
|
||||
: formatDate(this.endDate, this.hass.locale, this.hass.config)}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.date-range-picker.end_date"
|
||||
)}
|
||||
|
@ -34,6 +34,8 @@ const getValue = (obj, item) =>
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
const getWarning = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
@customElement("ha-form")
|
||||
export class HaForm extends LitElement implements HaFormElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@ -44,10 +46,14 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
|
||||
@property() public error?: Record<string, string>;
|
||||
|
||||
@property() public warning?: Record<string, string>;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property() public computeError?: (schema: any, error) => string;
|
||||
|
||||
@property() public computeWarning?: (schema: any, warning) => string;
|
||||
|
||||
@property() public computeLabel?: (
|
||||
schema: any,
|
||||
data: HaFormDataContainer
|
||||
@ -98,6 +104,7 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
: ""}
|
||||
${this.schema.map((item) => {
|
||||
const error = getError(this.error, item);
|
||||
const warning = getWarning(this.warning, item);
|
||||
|
||||
return html`
|
||||
${error
|
||||
@ -106,6 +113,12 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
${this._computeError(error, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: warning
|
||||
? html`
|
||||
<ha-alert own-margin alert-type="warning">
|
||||
${this._computeWarning(warning, item)}
|
||||
</ha-alert>
|
||||
`
|
||||
: ""}
|
||||
${"selector" in item
|
||||
? html`<ha-selector
|
||||
@ -187,6 +200,13 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
return this.computeError ? this.computeError(error, schema) : error;
|
||||
}
|
||||
|
||||
private _computeWarning(
|
||||
warning,
|
||||
schema: HaFormSchema | readonly HaFormSchema[]
|
||||
) {
|
||||
return this.computeWarning ? this.computeWarning(warning, schema) : warning;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
.root > * {
|
||||
|
@ -109,7 +109,8 @@ class HaHLSPlayer extends LitElement {
|
||||
private async _startHls(): Promise<void> {
|
||||
const masterPlaylistPromise = fetch(this.url);
|
||||
|
||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light")).default;
|
||||
const Hls: typeof HlsType = (await import("hls.js/dist/hls.light.mjs"))
|
||||
.default;
|
||||
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
|
@ -186,9 +186,8 @@ class HaHsColorPicker extends LitElement {
|
||||
}
|
||||
if (changedProps.has("value")) {
|
||||
if (
|
||||
this.value !== undefined &&
|
||||
(this._localValue?.[0] !== this.value[0] ||
|
||||
this._localValue?.[1] !== this.value[1])
|
||||
this._localValue?.[0] !== this.value?.[0] ||
|
||||
this._localValue?.[1] !== this.value?.[1]
|
||||
) {
|
||||
this._resetPosition();
|
||||
}
|
||||
@ -243,7 +242,11 @@ class HaHsColorPicker extends LitElement {
|
||||
}
|
||||
|
||||
private _resetPosition() {
|
||||
if (this.value === undefined) return;
|
||||
if (this.value === undefined) {
|
||||
this._cursorPosition = undefined;
|
||||
this._localValue = undefined;
|
||||
return;
|
||||
}
|
||||
this._cursorPosition = this._getCoordsFromValue(this.value);
|
||||
this._localValue = this.value;
|
||||
}
|
||||
@ -373,17 +376,20 @@ class HaHsColorPicker extends LitElement {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
outline: none;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
}
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
svg {
|
||||
position: absolute;
|
||||
|
137
src/components/ha-humidifier-state.ts
Normal file
137
src/components/ha-humidifier-state.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
|
||||
import { computeStateDisplay } from "../common/entity/compute_state_display";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforePercent } from "../common/translations/blank_before_percent";
|
||||
import { isUnavailableState, OFF } from "../data/entity";
|
||||
import { HumidifierEntity } from "../data/humidifier";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-humidifier-state")
|
||||
class HaHumidifierState extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HumidifierEntity;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const currentStatus = this._computeCurrentStatus();
|
||||
|
||||
return html`<div class="target">
|
||||
${!isUnavailableState(this.stateObj.state)
|
||||
? html`<span class="state-label">
|
||||
${this._localizeState()}
|
||||
${this.stateObj.attributes.mode
|
||||
? html`-
|
||||
${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"mode"
|
||||
)}`
|
||||
: ""}
|
||||
</span>
|
||||
<div class="unit">${this._computeTarget()}</div>`
|
||||
: this._localizeState()}
|
||||
</div>
|
||||
|
||||
${currentStatus && !isUnavailableState(this.stateObj.state)
|
||||
? html`<div class="current">
|
||||
${this.hass.localize("ui.card.climate.currently")}:
|
||||
<div class="unit">${currentStatus}</div>
|
||||
</div>`
|
||||
: ""}`;
|
||||
}
|
||||
|
||||
private _computeCurrentStatus(): string | undefined {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.current_humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.current_humidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _computeTarget(): string {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (this.stateObj.attributes.humidity != null) {
|
||||
return `${formatNumber(
|
||||
this.stateObj.attributes.humidity,
|
||||
this.hass.locale
|
||||
)}${blankBeforePercent(this.hass.locale)}%`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private _localizeState(): string {
|
||||
if (isUnavailableState(this.stateObj.state)) {
|
||||
return this.hass.localize(`state.default.${this.stateObj.state}`);
|
||||
}
|
||||
|
||||
const stateString = computeStateDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
return this.stateObj.attributes.action && this.stateObj.state !== OFF
|
||||
? `${computeAttributeValueDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.locale,
|
||||
this.hass.config,
|
||||
this.hass.entities,
|
||||
"action"
|
||||
)} (${stateString})`
|
||||
: stateString;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.current {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.unit {
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-humidifier-state": HaHumidifierState;
|
||||
}
|
||||
}
|
38
src/components/ha-icon-button-group.ts
Normal file
38
src/components/ha-icon-button-group.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-icon-button-group")
|
||||
export class HaIconButtonGroup extends LitElement {
|
||||
protected render(): TemplateResult {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
border-radius: 28px;
|
||||
background-color: rgba(139, 145, 151, 0.1);
|
||||
box-sizing: border-box;
|
||||
width: auto;
|
||||
padding: 4px;
|
||||
gap: 4px;
|
||||
}
|
||||
::slotted(.separator) {
|
||||
background-color: rgba(var(--rgb-primary-text-color), 0.15);
|
||||
width: 1px;
|
||||
height: 40px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-button-group": HaIconButtonGroup;
|
||||
}
|
||||
}
|
52
src/components/ha-icon-button-toggle.ts
Normal file
52
src/components/ha-icon-button-toggle.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import { css, CSSResultGroup } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaIconButton } from "./ha-icon-button";
|
||||
|
||||
@customElement("ha-icon-button-toggle")
|
||||
export class HaIconButtonToggle extends HaIconButton {
|
||||
@property({ type: Boolean, reflect: true }) selected = false;
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
position: relative;
|
||||
}
|
||||
mwc-icon-button {
|
||||
position: relative;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
mwc-icon-button::before {
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background-color: var(--primary-text-color);
|
||||
border-radius: 20px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
margin: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
:host([border-only]) mwc-icon-button::before {
|
||||
background-color: transparent;
|
||||
border: 2px solid var(--primary-text-color);
|
||||
}
|
||||
:host([selected]) mwc-icon-button {
|
||||
color: var(--primary-background-color);
|
||||
}
|
||||
:host([selected]) mwc-icon-button::before {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-icon-button-toggle": HaIconButtonToggle;
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@ import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { renderMarkdown } from "../resources/render-markdown";
|
||||
|
||||
const _blockQuoteToAlert = { Note: "info", Warning: "warning" };
|
||||
|
||||
@customElement("ha-markdown-element")
|
||||
class HaMarkdownElement extends ReactiveElement {
|
||||
@property() public content?;
|
||||
@ -65,6 +67,34 @@ class HaMarkdownElement extends ReactiveElement {
|
||||
node.loading = "lazy";
|
||||
}
|
||||
node.addEventListener("load", this._resize);
|
||||
} else if (node instanceof HTMLQuoteElement) {
|
||||
// Map GitHub blockquote elements to our ha-alert element
|
||||
const firstElementChild = node.firstElementChild;
|
||||
const quoteTitleElement = firstElementChild?.firstElementChild;
|
||||
const quoteType =
|
||||
quoteTitleElement?.textContent &&
|
||||
_blockQuoteToAlert[quoteTitleElement.textContent];
|
||||
|
||||
// GitHub is strict on how these are defined, we need to make sure we know what we have before starting to replace it
|
||||
if (quoteTitleElement?.nodeName === "STRONG" && quoteType) {
|
||||
const alertNote = document.createElement("ha-alert");
|
||||
alertNote.alertType = quoteType;
|
||||
alertNote.title =
|
||||
(firstElementChild!.childNodes[1].nodeName === "#text" &&
|
||||
firstElementChild!.childNodes[1].textContent?.trimStart()) ||
|
||||
"";
|
||||
|
||||
const childNodes = Array.from(firstElementChild!.childNodes);
|
||||
for (const child of childNodes.slice(
|
||||
childNodes.findIndex(
|
||||
// There is always a line break between the title and the content, we want to skip that
|
||||
(childNode) => childNode instanceof HTMLBRElement
|
||||
) + 1
|
||||
)) {
|
||||
alertNote.appendChild(child);
|
||||
}
|
||||
node.firstElementChild!.replaceWith(alertNote);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -73,20 +73,25 @@ class HaMenuButton extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
const oldNarrow =
|
||||
changedProps.get("narrow") ||
|
||||
(oldHass && oldHass.dockedSidebar === "always_hidden");
|
||||
const newNarrow =
|
||||
const oldHass = changedProps.has("hass")
|
||||
? (changedProps.get("hass") as HomeAssistant | undefined)
|
||||
: this.hass;
|
||||
const oldNarrow = changedProps.has("narrow")
|
||||
? (changedProps.get("narrow") as boolean | undefined)
|
||||
: this.narrow;
|
||||
|
||||
const oldShowButton =
|
||||
oldNarrow || oldHass?.dockedSidebar === "always_hidden";
|
||||
const showButton =
|
||||
this.narrow || this.hass.dockedSidebar === "always_hidden";
|
||||
|
||||
if (oldNarrow === newNarrow) {
|
||||
if (oldShowButton === showButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
|
||||
this.style.display = showButton || this._alwaysVisible ? "initial" : "none";
|
||||
|
||||
if (!newNarrow) {
|
||||
if (!showButton) {
|
||||
if (this._unsubNotifications) {
|
||||
this._unsubNotifications();
|
||||
this._unsubNotifications = undefined;
|
||||
@ -98,6 +103,9 @@ class HaMenuButton extends LitElement {
|
||||
}
|
||||
|
||||
private _subscribeNotifications() {
|
||||
if (this._unsubNotifications) {
|
||||
throw new Error("Already subscribed");
|
||||
}
|
||||
this._unsubNotifications = subscribeNotifications(
|
||||
this.hass.connection,
|
||||
(notifications) => {
|
||||
|
@ -2,7 +2,7 @@ import { mdiImagePlus } from "@mdi/js";
|
||||
import { html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image";
|
||||
import { createImage, generateImageThumbnailUrl } from "../data/image_upload";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import {
|
||||
CropOptions,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user