feat: Add hardened error handling and retries (#2451)
- Closes #1830 - Closes #2099
This commit is contained in:
committed by
dermotduffy
parent
4bc787e2b7
commit
47bcce93d3
@@ -1,15 +1,14 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { until } from 'lit/directives/until.js';
|
||||
import { ProblemPresence } from '../card-controller/problems/types';
|
||||
import { IssuePresence } from '../card-controller/issues/types';
|
||||
import { RawAdvancedCameraCardConfig } from '../config/types';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import basicBlockStyle from '../scss/basic-block.scss';
|
||||
import { getDiagnostics } from '../utils/diagnostics';
|
||||
import { renderMessage } from './message';
|
||||
|
||||
import { renderNotificationBlockFromText } from './notification/block';
|
||||
@customElement('advanced-camera-card-diagnostics')
|
||||
export class AdvancedCameraCardDiagnostics extends LitElement {
|
||||
// Not a reactive property to avoid multiple diagnostics fetches.
|
||||
@@ -22,18 +21,17 @@ export class AdvancedCameraCardDiagnostics extends LitElement {
|
||||
public rawConfig?: RawAdvancedCameraCardConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
public problems?: ProblemPresence;
|
||||
public issues?: IssuePresence;
|
||||
|
||||
private async _renderDiagnostics(): Promise<TemplateResult> {
|
||||
const diagnostics = await getDiagnostics(
|
||||
this.hass,
|
||||
this.deviceRegistryManager,
|
||||
this.rawConfig,
|
||||
this.problems,
|
||||
this.issues,
|
||||
);
|
||||
|
||||
return renderMessage({
|
||||
message: localize('error.diagnostics'),
|
||||
return renderNotificationBlockFromText(localize('error.diagnostics'), {
|
||||
icon: 'mdi:cogs',
|
||||
context: diagnostics,
|
||||
});
|
||||
@@ -42,10 +40,9 @@ export class AdvancedCameraCardDiagnostics extends LitElement {
|
||||
protected render(): TemplateResult | void {
|
||||
return html`${until(
|
||||
this._renderDiagnostics(),
|
||||
renderMessage({
|
||||
message: localize('error.fetching_diagnostics'),
|
||||
dotdotdot: true,
|
||||
renderNotificationBlockFromText(localize('error.fetching_diagnostics'), {
|
||||
icon: 'mdi:cogs',
|
||||
in_progress: true,
|
||||
}),
|
||||
)}`;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { IssueTriggerEventData } from '../card-controller/issues/types.js';
|
||||
import { TemplateRenderer } from '../card-controller/templates/index.js';
|
||||
import { dispatchAdvancedCameraCardErrorEvent } from '../components-lib/message/dispatch.js';
|
||||
import { ConditionsManager } from '../conditions/conditions-manager.js';
|
||||
import { getConditionStateManagerViaEvent } from '../conditions/state-manager-via-event.js';
|
||||
import { ConditionStateManager } from '../conditions/state-manager.js';
|
||||
@@ -35,6 +35,12 @@ import { AdvancedCameraCardError } from '../types.js';
|
||||
import { errorToConsole } from '../utils/basic.js';
|
||||
import { fireAdvancedCameraCardEvent } from '../utils/fire-advanced-camera-card-event.js';
|
||||
|
||||
class ElementsCreationError extends AdvancedCameraCardError {
|
||||
constructor(context?: unknown) {
|
||||
super(localize('error.could_not_create_elements'), context);
|
||||
}
|
||||
}
|
||||
|
||||
/* A note on picture element rendering:
|
||||
*
|
||||
* To avoid needing to deal with the rendering of all the picture elements
|
||||
@@ -106,7 +112,7 @@ export class AdvancedCameraCardElementsCore extends LitElement {
|
||||
private _createRoot(): HuiConditionalElement {
|
||||
const elementConstructor = customElements.get('hui-conditional-element');
|
||||
if (!elementConstructor || !this.hass) {
|
||||
throw new Error(localize('error.could_not_render_elements'));
|
||||
throw new ElementsCreationError(this._renderedElements);
|
||||
}
|
||||
|
||||
const element = new elementConstructor() as HuiConditionalElement;
|
||||
@@ -145,7 +151,11 @@ export class AdvancedCameraCardElementsCore extends LitElement {
|
||||
this._renderedElements = elements;
|
||||
this._root = this._createRoot();
|
||||
} catch (e) {
|
||||
return dispatchAdvancedCameraCardErrorEvent(this, e as AdvancedCameraCardError);
|
||||
errorToConsole(e as Error);
|
||||
fireAdvancedCameraCardEvent<IssueTriggerEventData>(this, 'issue:trigger', {
|
||||
key: 'config_error',
|
||||
error: new ElementsCreationError(elements),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@ import { THUMBNAIL_WIDTH_DEFAULT } from '../../config/schema/common/controls/thu
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import galleryCoreStyle from '../../scss/gallery-core.scss';
|
||||
import '../message.js';
|
||||
import '../progress-indicator.js';
|
||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import { MediaGalleryConfig } from '../../config/schema/media-gallery.js';
|
||||
import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../../const.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import galleryStyle from '../../scss/gallery.scss';
|
||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
@@ -36,8 +35,7 @@ import { UnifiedQueryBuilder } from '../../view/unified-query-builder.js';
|
||||
import { UnifiedQueryRunner } from '../../view/unified-query-runner.js';
|
||||
import { getReviewedQueryFilterFromQuery } from '../../view/utils/query-filter.js';
|
||||
import '../media-filter.js';
|
||||
import '../message.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import { renderNoMedia } from '../notification/no-media.js';
|
||||
import '../surround-basic.js';
|
||||
import '../thumbnail/thumbnail.js';
|
||||
import './gallery-core.js';
|
||||
@@ -216,13 +214,10 @@ export class AdvancedCameraCardGallery extends LitElement {
|
||||
</advanced-camera-card-media-filter>`
|
||||
: ''}
|
||||
${!hasItems
|
||||
? renderMessage({
|
||||
type: 'info',
|
||||
message: isLoading
|
||||
? localize('error.awaiting_media')
|
||||
: localize('common.no_media'),
|
||||
icon: 'mdi:multimedia',
|
||||
dotdotdot: isLoading,
|
||||
? renderNoMedia({
|
||||
cameraID: this.viewManagerEpoch?.manager.getView()?.camera ?? null,
|
||||
cameraManager: this.cameraManager ?? null,
|
||||
loading: isLoading,
|
||||
})
|
||||
: html`<advanced-camera-card-gallery-core
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -12,32 +12,33 @@ import { live } from 'lit/directives/live.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { getCameraEntityFromConfig } from '../camera-manager/utils/camera-entity-from-config.js';
|
||||
import { IssueTriggerEventData } from '../card-controller/issues/types.js';
|
||||
import { CachedValueController } from '../components-lib/cached-value-controller.js';
|
||||
import { UpdatingImageMediaPlayerController } from '../components-lib/media-player/updating-image.js';
|
||||
import { dataToContext } from '../components-lib/notification/data-to-context.js';
|
||||
import { SignedURLController } from '../components-lib/signed-url-controller.js';
|
||||
import { Notification } from '../config/schema/actions/types.js';
|
||||
import { CameraConfig } from '../config/schema/cameras.js';
|
||||
import { type ImageBaseConfig, ImageMode } from '../config/schema/common/image.js';
|
||||
import { EnabledProxyConfig } from '../config/schema/common/proxy.js';
|
||||
import { TROUBLESHOOTING_URL } from '../const.js';
|
||||
import { isHassDifferent } from '../ha/is-hass-different.js';
|
||||
import { HomeAssistant } from '../ha/types.js';
|
||||
import defaultImage from '../images/iris-screensaver.jpg';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import imageUpdatingPlayerStyle from '../scss/image-updating-player.scss';
|
||||
import {
|
||||
MediaLoadedInfo,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
Message,
|
||||
} from '../types.js';
|
||||
import { MediaLoadedInfo, MediaPlayer, MediaPlayerController } from '../types.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
import { fireAdvancedCameraCardEvent } from '../utils/fire-advanced-camera-card-event.js';
|
||||
import {
|
||||
createMediaLoadedInfo,
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
dispatchMediaPlayEvent,
|
||||
} from '../utils/media-info.js';
|
||||
import { IMAGE_VIEW_TARGET_ID_SENTINEL } from '../view/target-id.js';
|
||||
import { View } from '../view/view.js';
|
||||
import { renderMessage } from './message.js';
|
||||
import { renderNotificationBlock } from './notification/block.js';
|
||||
|
||||
// See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py .
|
||||
const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000;
|
||||
@@ -109,10 +110,21 @@ export class AdvancedCameraCardImageUpdatingPlayer
|
||||
() => this._getImageSource(),
|
||||
() => dispatchMediaPlayEvent(this),
|
||||
() => dispatchMediaPauseEvent(this),
|
||||
// Clear image load errors on each timer tick so the next render retries the
|
||||
// <img>. Retries are bounded by refresh_seconds, not a tight loop.
|
||||
// Clear image load errors on each timer tick so the next render retries
|
||||
// the <img> — but only for modes where the underlying URL genuinely
|
||||
// changes between ticks (camera/entity snapshots). For mode: url, the
|
||||
// same static URL will fail the same way every time, so clearing the
|
||||
// error just causes a visible flicker (notification → blank <img> →
|
||||
// notification) every refresh_seconds. URL-mode retries are driven by
|
||||
// mediaEpoch (user-initiated or auto-retry) instead.
|
||||
() => {
|
||||
this._imageLoadError = false;
|
||||
const mode = resolveImageMode({
|
||||
imageConfig: this.imageConfig,
|
||||
cameraConfig: this.cameraConfig,
|
||||
});
|
||||
if (mode !== 'url') {
|
||||
this._imageLoadError = false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -393,31 +405,35 @@ export class AdvancedCameraCardImageUpdatingPlayer
|
||||
}
|
||||
}
|
||||
|
||||
private _getDisplayMessage(): Message | null {
|
||||
private _getDisplayNotification(): Notification | null {
|
||||
const error = this._signedURLController.getError();
|
||||
if (error) {
|
||||
return {
|
||||
type: 'error',
|
||||
message: localize(
|
||||
error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign',
|
||||
),
|
||||
context: this.proxyConfig,
|
||||
heading: {
|
||||
text: localize(error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign'),
|
||||
icon: 'mdi:alert-circle',
|
||||
},
|
||||
link: { url: TROUBLESHOOTING_URL, title: localize('error.troubleshooting') },
|
||||
context: this.proxyConfig ? dataToContext(this.proxyConfig) : undefined,
|
||||
};
|
||||
}
|
||||
if (this._imageLoadError) {
|
||||
return {
|
||||
type: 'error',
|
||||
message: localize('error.image_load_error'),
|
||||
context: this.imageConfig,
|
||||
heading: {
|
||||
text: localize('error.image_load_error'),
|
||||
icon: 'mdi:alert-circle',
|
||||
},
|
||||
link: { url: TROUBLESHOOTING_URL, title: localize('error.troubleshooting') },
|
||||
context: this.imageConfig ? dataToContext(this.imageConfig) : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const message = this._getDisplayMessage();
|
||||
if (message) {
|
||||
return renderMessage(message);
|
||||
const notification = this._getDisplayNotification();
|
||||
if (notification) {
|
||||
return renderNotificationBlock(notification);
|
||||
}
|
||||
|
||||
const src = this._cachedValueController?.getValue();
|
||||
@@ -449,13 +465,14 @@ export class AdvancedCameraCardImageUpdatingPlayer
|
||||
cameraConfig: this.cameraConfig,
|
||||
});
|
||||
if (mode === 'camera' || mode === 'entity' || mode === 'screensaver') {
|
||||
// In camera, entity, or screensaver mode the user has likely
|
||||
// not made an error, but the source may be unavailable, so show
|
||||
// the stock image.
|
||||
this._forceSafeImage(true);
|
||||
} else if (mode === 'url') {
|
||||
this._imageLoadError = true;
|
||||
}
|
||||
fireAdvancedCameraCardEvent<IssueTriggerEventData>(this, 'issue:trigger', {
|
||||
key: 'media_load',
|
||||
targetID: IMAGE_VIEW_TARGET_ID_SENTINEL,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
`
|
||||
|
||||
+23
-15
@@ -1,6 +1,7 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types';
|
||||
@@ -12,15 +13,15 @@ import {
|
||||
resolveProxyConfig,
|
||||
} from '../config/schema/common/proxy';
|
||||
import { ImageViewConfig, type ImageViewProxyConfig } from '../config/schema/image';
|
||||
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import imageStyle from '../scss/image.scss';
|
||||
import { MediaPlayer, MediaPlayerController, MediaPlayerElement } from '../types.js';
|
||||
import { IMAGE_VIEW_TARGET_ID_SENTINEL } from '../view/target-id.js';
|
||||
import './image-updating-player';
|
||||
import { resolveImageMode } from './image-updating-player';
|
||||
import './media-dimensions-container';
|
||||
import { renderMessage } from './message.js';
|
||||
import { renderNotificationBlockFromText } from './notification/block.js';
|
||||
import './zoomer.js';
|
||||
|
||||
@customElement('advanced-camera-card-image')
|
||||
@@ -48,7 +49,7 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
||||
}
|
||||
|
||||
private _renderContainer(template: TemplateResult): TemplateResult {
|
||||
const zoomTarget = IMAGE_VIEW_ZOOM_TARGET_SENTINEL;
|
||||
const zoomTarget = IMAGE_VIEW_TARGET_ID_SENTINEL;
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const mode = resolveImageMode({
|
||||
imageConfig: this.imageConfig,
|
||||
@@ -108,23 +109,30 @@ export class AdvancedCameraCardImage extends LitElement implements MediaPlayer {
|
||||
});
|
||||
|
||||
if (mode === 'camera' && !this.cameraConfig) {
|
||||
return renderMessage({
|
||||
type: 'info',
|
||||
message: localize('error.no_camera_for_image'),
|
||||
return renderNotificationBlockFromText(localize('error.no_camera_for_image'), {
|
||||
icon: 'mdi:camera-off',
|
||||
});
|
||||
}
|
||||
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const mediaEpoch = view?.context?.mediaEpoch?.[IMAGE_VIEW_TARGET_ID_SENTINEL] ?? 0;
|
||||
|
||||
return this._renderContainer(html`
|
||||
<advanced-camera-card-image-updating-player
|
||||
${ref(this._refImage)}
|
||||
.hass=${this.hass}
|
||||
.view=${this.viewManagerEpoch?.manager.getView()}
|
||||
.imageConfig=${this.imageConfig}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.proxyConfig=${this._resolveProxyConfig(this.imageConfig?.proxy) ?? undefined}
|
||||
>
|
||||
</advanced-camera-card-image-updating-player>
|
||||
${keyed(
|
||||
mediaEpoch,
|
||||
html`
|
||||
<advanced-camera-card-image-updating-player
|
||||
${ref(this._refImage)}
|
||||
.hass=${this.hass}
|
||||
.view=${view}
|
||||
.imageConfig=${this.imageConfig}
|
||||
.cameraConfig=${this.cameraConfig}
|
||||
.proxyConfig=${this._resolveProxyConfig(this.imageConfig?.proxy) ??
|
||||
undefined}
|
||||
>
|
||||
</advanced-camera-card-image-updating-player>
|
||||
`,
|
||||
)}
|
||||
`);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { CameraManagerCameraMetadata } from '../../camera-manager/types.js';
|
||||
@@ -220,32 +221,38 @@ export class AdvancedCameraCardLiveCarousel extends LitElement {
|
||||
|
||||
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
const mediaEpoch = view?.context?.mediaEpoch?.[cameraID] ?? 0;
|
||||
|
||||
return html`
|
||||
<div class="embla__slide">
|
||||
<advanced-camera-card-live-provider
|
||||
.microphoneState=${view?.camera === cameraID
|
||||
? this.microphoneState
|
||||
: undefined}
|
||||
.camera=${camera}
|
||||
.cameraEndpoints=${guard(
|
||||
[this.cameraManager, cameraID],
|
||||
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
||||
)}
|
||||
.label=${cameraMetadata?.title ?? ''}
|
||||
.liveConfig=${this.liveConfig}
|
||||
.hass=${this.hass}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
|
||||
.zoom=${!this._isGesturesPTZActive(view, cameraID)}
|
||||
@advanced-camera-card:zoom:change=${(ev: CustomEvent<ZoomSettingsObserved>) =>
|
||||
handleZoomSettingsObservedEvent(
|
||||
ev,
|
||||
this.viewManagerEpoch?.manager,
|
||||
cameraID,
|
||||
${keyed(
|
||||
mediaEpoch,
|
||||
html`<advanced-camera-card-live-provider
|
||||
.microphoneState=${view?.camera === cameraID
|
||||
? this.microphoneState
|
||||
: undefined}
|
||||
.camera=${camera}
|
||||
.cameraEndpoints=${guard(
|
||||
[this.cameraManager, cameraID],
|
||||
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
||||
)}
|
||||
>
|
||||
</advanced-camera-card-live-provider>
|
||||
.label=${cameraMetadata?.title ?? ''}
|
||||
.liveConfig=${this.liveConfig}
|
||||
.hass=${this.hass}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.zoomSettings=${view?.context?.zoom?.[cameraID]?.requested}
|
||||
.zoom=${!this._isGesturesPTZActive(view, cameraID)}
|
||||
@advanced-camera-card:zoom:change=${(
|
||||
ev: CustomEvent<ZoomSettingsObserved>,
|
||||
) =>
|
||||
handleZoomSettingsObservedEvent(
|
||||
ev,
|
||||
this.viewManagerEpoch?.manager,
|
||||
cameraID,
|
||||
)}
|
||||
>
|
||||
</advanced-camera-card-live-provider>`,
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { fireAdvancedCameraCardEvent } from '../../utils/fire-advanced-camera-ca
|
||||
import { getResolvedLiveProvider } from '../../utils/live-provider.js';
|
||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||
import '../icon.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import { renderNotificationBlockFromText } from '../notification/block.js';
|
||||
import './../media-dimensions-container';
|
||||
|
||||
@customElement('advanced-camera-card-live-provider')
|
||||
@@ -73,6 +73,12 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
@state()
|
||||
private _hasProviderError = false;
|
||||
|
||||
// Whether the camera entity has ever been in a non-unavailable state. Used
|
||||
// to suppress transient unavailability errors for entities that were
|
||||
// previously working (e.g. during PTZ operations).
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2124
|
||||
private _entityHasBeenAvailable = false;
|
||||
|
||||
private _refProvider: Ref<MediaPlayerElement> = createRef();
|
||||
|
||||
private _lazyLoadController: LazyLoadController = new LazyLoadController(this);
|
||||
@@ -121,6 +127,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
this._isVideoMediaLoaded = false;
|
||||
this._entityHasBeenAvailable = false;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -132,11 +139,12 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
ev.stopPropagation();
|
||||
this._hasProviderError = true;
|
||||
|
||||
const cameraID = this.camera?.getID();
|
||||
if (cameraID) {
|
||||
fireAdvancedCameraCardEvent(this, 'problem:trigger', {
|
||||
key: 'stream_not_loading' as const,
|
||||
cameraID,
|
||||
// this.camera is already substream-aware (resolved by the carousel layer).
|
||||
const targetID = this.camera?.getID();
|
||||
if (targetID) {
|
||||
fireAdvancedCameraCardEvent(this, 'issue:trigger', {
|
||||
key: 'media_load' as const,
|
||||
targetID,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -162,6 +170,10 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
}
|
||||
|
||||
if (changedProps.has('camera')) {
|
||||
this._isVideoMediaLoaded = false;
|
||||
this._hasProviderError = false;
|
||||
this._entityHasBeenAvailable = false;
|
||||
|
||||
const provider = getResolvedLiveProvider(this.camera?.getConfig());
|
||||
if (provider === 'jsmpeg') {
|
||||
this._importPromises.push(import('./providers/jsmpeg.js'));
|
||||
@@ -256,9 +268,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
) {
|
||||
if (!cameraConfig?.camera_entity) {
|
||||
dispatchLiveErrorEvent(this);
|
||||
return renderMessage({
|
||||
message: localize('error.no_live_camera'),
|
||||
type: 'error',
|
||||
return renderNotificationBlockFromText(localize('error.no_live_camera'), {
|
||||
icon: 'mdi:camera',
|
||||
context: cameraConfig,
|
||||
});
|
||||
@@ -267,25 +277,28 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
const stateObj = this.hass.states[cameraConfig.camera_entity];
|
||||
if (!stateObj) {
|
||||
dispatchLiveErrorEvent(this);
|
||||
return renderMessage({
|
||||
message: localize('error.live_camera_not_found'),
|
||||
type: 'error',
|
||||
return renderNotificationBlockFromText(localize('error.live_camera_not_found'), {
|
||||
icon: 'mdi:camera',
|
||||
context: cameraConfig,
|
||||
});
|
||||
}
|
||||
|
||||
if (stateObj.state === 'unavailable') {
|
||||
dispatchLiveErrorEvent(this);
|
||||
dispatchMediaUnloadedEvent(this);
|
||||
return renderMessage({
|
||||
message: `${localize('error.live_camera_unavailable')}${
|
||||
this.label ? `: ${this.label}` : ''
|
||||
}`,
|
||||
type: 'info',
|
||||
icon: 'mdi:cctv-off',
|
||||
dotdotdot: true,
|
||||
});
|
||||
if (
|
||||
!this._entityHasBeenAvailable ||
|
||||
cameraConfig.always_error_if_entity_unavailable
|
||||
) {
|
||||
dispatchLiveErrorEvent(this);
|
||||
dispatchMediaUnloadedEvent(this);
|
||||
return renderNotificationBlockFromText(
|
||||
`${localize('error.live_camera_unavailable')}${
|
||||
this.label ? `: ${this.label}` : ''
|
||||
}`,
|
||||
{ icon: 'mdi:cctv-off', in_progress: true },
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this._entityHasBeenAvailable = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,8 +386,7 @@ export class AdvancedCameraCardLiveProvider extends LitElement implements MediaP
|
||||
? html`<advanced-camera-card-icon
|
||||
title=${localize('error.awaiting_live')}
|
||||
.icon=${{ icon: 'mdi:progress-helper' }}
|
||||
@click=${() =>
|
||||
fireAdvancedCameraCardEvent(this, 'problem:notify', 'stream_not_loading')}
|
||||
@click=${() => fireAdvancedCameraCardEvent(this, 'issue:notify', 'media_load')}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}`;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import { HomeAssistant } from '../../../../ha/types.js';
|
||||
import { localize } from '../../../../localize/localize.js';
|
||||
import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
||||
import { MediaPlayer, MediaPlayerController } from '../../../../types.js';
|
||||
import { renderMessage } from '../../../message.js';
|
||||
import { renderNotificationBlockFromText } from '../../../notification/block.js';
|
||||
import { VideoRTC } from './video-rtc.js';
|
||||
|
||||
customElements.define('advanced-camera-card-live-go2rtc-player', VideoRTC);
|
||||
@@ -144,18 +144,13 @@ export class AdvancedCameraCardGo2RTC extends LitElement implements MediaPlayer
|
||||
protected render(): TemplateResult | void {
|
||||
const error = this._signedURLController.getError();
|
||||
if (error) {
|
||||
return renderMessage({
|
||||
type: 'error',
|
||||
message: localize(
|
||||
error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign',
|
||||
),
|
||||
context: this.camera?.getConfig(),
|
||||
});
|
||||
return renderNotificationBlockFromText(
|
||||
localize(error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign'),
|
||||
{ context: this.camera?.getConfig() },
|
||||
);
|
||||
}
|
||||
if (!this.cameraEndpoints?.go2rtc) {
|
||||
return renderMessage({
|
||||
type: 'error',
|
||||
message: localize('error.live_camera_no_endpoint'),
|
||||
return renderNotificationBlockFromText(localize('error.live_camera_no_endpoint'), {
|
||||
context: this.camera?.getConfig(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,13 +12,15 @@ import { until } from 'lit/directives/until.js';
|
||||
import { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { JSMPEGMediaPlayerController } from '../../../components-lib/media-player/jsmpeg.js';
|
||||
import { createNotificationFromText } from '../../../components-lib/notification/factory.js';
|
||||
import { Notification } from '../../../config/schema/actions/types.js';
|
||||
import { CameraConfig } from '../../../config/schema/cameras.js';
|
||||
import { CardWideConfig } from '../../../config/schema/types.js';
|
||||
import { homeAssistantGetSignedURLIfNecessary } from '../../../ha/sign-path.js';
|
||||
import { HomeAssistant } from '../../../ha/types.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
||||
import { MediaPlayer, MediaPlayerController, Message } from '../../../types.js';
|
||||
import { MediaPlayer, MediaPlayerController } from '../../../types.js';
|
||||
import { convertHTTPAdressToWebsocket, errorToConsole } from '../../../utils/basic.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
@@ -26,8 +28,8 @@ import {
|
||||
dispatchMediaPlayEvent,
|
||||
} from '../../../utils/media-info.js';
|
||||
import { Timer } from '../../../utils/timer.js';
|
||||
import '../../message.js';
|
||||
import { renderMessage } from '../../message.js';
|
||||
import '../../notification/block.js';
|
||||
import { renderNotificationBlock } from '../../notification/block.js';
|
||||
import '../../progress-indicator.js';
|
||||
import { renderProgressIndicator } from '../../progress-indicator.js';
|
||||
|
||||
@@ -51,7 +53,7 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
@state()
|
||||
private _message: Message | null = null;
|
||||
private _notification: Notification | null = null;
|
||||
|
||||
private _jsmpegCanvasElement?: HTMLCanvasElement;
|
||||
private _jsmpegVideoPlayer?: JSMpeg.VideoElement;
|
||||
@@ -71,7 +73,7 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
if (
|
||||
['cameraConfig', 'cameraEndpoints'].some((prop) => changedProperties.has(prop))
|
||||
) {
|
||||
this._message = null;
|
||||
this._notification = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +135,7 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
}
|
||||
|
||||
private _resetPlayer(): void {
|
||||
this._message = null;
|
||||
this._notification = null;
|
||||
this._refreshPlayerTimer.stop();
|
||||
if (this._jsmpegVideoPlayer) {
|
||||
try {
|
||||
@@ -175,11 +177,10 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
|
||||
const endpoint = this.cameraEndpoints?.jsmpeg;
|
||||
if (!endpoint) {
|
||||
this._message = {
|
||||
message: localize('error.live_camera_no_endpoint'),
|
||||
type: 'error',
|
||||
context: this.cameraConfig,
|
||||
};
|
||||
this._notification = createNotificationFromText(
|
||||
localize('error.live_camera_no_endpoint'),
|
||||
{ context: this.cameraConfig },
|
||||
);
|
||||
dispatchLiveErrorEvent(this);
|
||||
return;
|
||||
}
|
||||
@@ -197,11 +198,9 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
const address = response ? convertHTTPAdressToWebsocket(response) : null;
|
||||
|
||||
if (!address) {
|
||||
this._message = {
|
||||
type: 'error',
|
||||
message: localize('error.failed_sign'),
|
||||
this._notification = createNotificationFromText(localize('error.failed_sign'), {
|
||||
context: this.cameraConfig,
|
||||
};
|
||||
});
|
||||
dispatchLiveErrorEvent(this);
|
||||
return;
|
||||
}
|
||||
@@ -214,20 +213,19 @@ export class AdvancedCameraCardLiveJSMPEG extends LitElement implements MediaPla
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (this._message) {
|
||||
return renderMessage(this._message);
|
||||
if (this._notification) {
|
||||
return renderNotificationBlock(this._notification);
|
||||
}
|
||||
|
||||
const _render = async (): Promise<TemplateResult | void> => {
|
||||
await this._refreshPlayer();
|
||||
|
||||
if (!this._jsmpegVideoPlayer || !this._jsmpegCanvasElement) {
|
||||
if (!this._message) {
|
||||
this._message = {
|
||||
message: localize('error.jsmpeg_no_player'),
|
||||
type: 'error',
|
||||
context: this.cameraConfig,
|
||||
};
|
||||
if (!this._notification) {
|
||||
this._notification = createNotificationFromText(
|
||||
localize('error.jsmpeg_no_player'),
|
||||
{ context: this.cameraConfig },
|
||||
);
|
||||
dispatchLiveErrorEvent(this);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -12,6 +12,8 @@ import { CameraEndpoints } from '../../../camera-manager/types.js';
|
||||
import { dispatchLiveErrorEvent } from '../../../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get-technology-for-video-rtc.js';
|
||||
import { VideoMediaPlayerController } from '../../../components-lib/media-player/video.js';
|
||||
import { createNotificationFromText } from '../../../components-lib/notification/factory.js';
|
||||
import { Notification } from '../../../config/schema/actions/types.js';
|
||||
import { CameraConfig } from '../../../config/schema/cameras.js';
|
||||
import { CardWideConfig } from '../../../config/schema/types.js';
|
||||
import { HomeAssistant } from '../../../ha/types.js';
|
||||
@@ -21,7 +23,6 @@ import {
|
||||
AdvancedCameraCardError,
|
||||
MediaPlayer,
|
||||
MediaPlayerController,
|
||||
Message,
|
||||
} from '../../../types.js';
|
||||
import { mayHaveAudio } from '../../../utils/audio.js';
|
||||
import {
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
MEDIA_LOAD_CONTROLS_HIDE_SECONDS,
|
||||
setControlsOnVideo,
|
||||
} from '../../../utils/controls.js';
|
||||
import { getContextFromError } from '../../../utils/error-context.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
dispatchMediaPauseEvent,
|
||||
@@ -36,8 +38,8 @@ import {
|
||||
dispatchMediaVolumeChangeEvent,
|
||||
} from '../../../utils/media-info.js';
|
||||
import { renderTask } from '../../../utils/task.js';
|
||||
import '../../message.js';
|
||||
import { renderMessage } from '../../message.js';
|
||||
import '../../notification/block.js';
|
||||
import { renderNotificationBlock } from '../../notification/block.js';
|
||||
import '../../progress-indicator.js';
|
||||
import { renderProgressIndicator } from '../../progress-indicator.js';
|
||||
import { VideoRTC } from './go2rtc/video-rtc.js';
|
||||
@@ -59,7 +61,7 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi
|
||||
public controls = false;
|
||||
|
||||
@state()
|
||||
private _message: Message | null = null;
|
||||
private _notification: Notification | null = null;
|
||||
|
||||
private hass?: HomeAssistant;
|
||||
|
||||
@@ -88,7 +90,7 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._videoRTC = null;
|
||||
this._message = null;
|
||||
this._notification = null;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
@@ -96,7 +98,7 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi
|
||||
if (
|
||||
['cameraConfig', 'cameraEndpoints'].some((prop) => changedProperties.has(prop))
|
||||
) {
|
||||
this._message = null;
|
||||
this._notification = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,8 +150,8 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (this._message) {
|
||||
return renderMessage(this._message);
|
||||
if (this._notification) {
|
||||
return renderNotificationBlock(this._notification);
|
||||
}
|
||||
|
||||
const render = (): TemplateResult | void => {
|
||||
@@ -157,16 +159,15 @@ export class AdvancedCameraCardLiveWebRTCCard extends LitElement implements Medi
|
||||
try {
|
||||
webrtcElement = this._createWebRTC();
|
||||
} catch (e) {
|
||||
this._message = {
|
||||
type: 'error',
|
||||
message:
|
||||
e instanceof AdvancedCameraCardError
|
||||
? e.message
|
||||
: localize('error.webrtc_card_reported_error') +
|
||||
': ' +
|
||||
(e as Error).message,
|
||||
context: (e as AdvancedCameraCardError).context,
|
||||
};
|
||||
const context = getContextFromError(e);
|
||||
this._notification = createNotificationFromText(
|
||||
e instanceof AdvancedCameraCardError
|
||||
? e.message
|
||||
: localize('error.webrtc_card_reported_error') + ': ' + (e as Error).message,
|
||||
{
|
||||
...(context && { context }),
|
||||
},
|
||||
);
|
||||
dispatchLiveErrorEvent(this);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { MessageController } from '../components-lib/message/controller.js';
|
||||
import messageStyle from '../scss/message.scss';
|
||||
import { Message } from '../types.js';
|
||||
import './icon.js';
|
||||
|
||||
export function renderMessage(
|
||||
message: Message | null,
|
||||
renderOptions?: {
|
||||
overlay?: boolean;
|
||||
},
|
||||
): TemplateResult {
|
||||
return html` <advanced-camera-card-message
|
||||
.message=${message}
|
||||
?overlay=${!!renderOptions?.overlay}
|
||||
></advanced-camera-card-message>`;
|
||||
}
|
||||
@customElement('advanced-camera-card-message')
|
||||
export class AdvancedCameraCardMessage extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public message?: Message;
|
||||
|
||||
@property({ attribute: true, type: Boolean })
|
||||
public overlay = false;
|
||||
|
||||
private _controller = new MessageController();
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = this._controller.getLink(this.message);
|
||||
const messageTemplate = html`
|
||||
${this._controller.getMessageString(this.message)}
|
||||
${link ? html`. <a href="${link.url}">${link.title}</a>` : ''}
|
||||
`;
|
||||
|
||||
const icon = this._controller.getIcon(this.message);
|
||||
const classes = {
|
||||
dotdotdot: !!this.message?.dotdotdot,
|
||||
};
|
||||
|
||||
return html` <div class="wrapper">
|
||||
<div class="message padded">
|
||||
<div class="icon">
|
||||
<advanced-camera-card-icon
|
||||
part="icon"
|
||||
.icon="${{ icon: icon }}"
|
||||
></advanced-camera-card-icon>
|
||||
</div>
|
||||
<div class="contents">
|
||||
<span class="${classMap(classes)}">${messageTemplate}</span>
|
||||
${this._controller
|
||||
.getContextStrings(this.message)
|
||||
.map((contextItem) => html`<pre>${contextItem}</pre>`)}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(messageStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-message': AdvancedCameraCardMessage;
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { actionHandler } from '../action-handler-directive.js';
|
||||
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request';
|
||||
import {
|
||||
Notification,
|
||||
NotificationControl,
|
||||
NotificationDetail,
|
||||
} from '../config/schema/actions/types.js';
|
||||
import notificationStyle from '../scss/notification.scss';
|
||||
import {
|
||||
getActionConfigGivenAction,
|
||||
hasAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../utils/action.js';
|
||||
import { arrayify } from '../utils/basic.js';
|
||||
import { dispatchDismissNotificationEvent } from '../utils/notification.js';
|
||||
import './icon.js';
|
||||
|
||||
@customElement('advanced-camera-card-notification')
|
||||
export class AdvancedCameraCardNotification extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public notification: Notification | null = null;
|
||||
|
||||
private _refNotification: Ref<HTMLElement> = createRef();
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener('click', this._handleOutsideInteraction);
|
||||
window.addEventListener('focusin', this._handleOutsideInteraction);
|
||||
window.addEventListener('keydown', this._handleKeyDown);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
window.removeEventListener('click', this._handleOutsideInteraction);
|
||||
window.removeEventListener('focusin', this._handleOutsideInteraction);
|
||||
window.removeEventListener('keydown', this._handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const heading = this.notification.heading;
|
||||
const details = this.notification.details ?? [];
|
||||
const text = this.notification.text;
|
||||
const controls = this.notification.controls ?? [];
|
||||
|
||||
return html`
|
||||
<div class="backdrop" @click=${this._dismiss}></div>
|
||||
<div
|
||||
class="notification"
|
||||
${ref(this._refNotification)}
|
||||
@animationend=${this._handleAnimationEnd}
|
||||
>
|
||||
<div class="details">
|
||||
${heading ? this._renderDetail(heading, true) : ''}
|
||||
${details.map((detail) => this._renderDetail(detail))}
|
||||
${text ? html`<div class="description">${text}</div>` : ''}
|
||||
${this.notification.link
|
||||
? html`<div class="url">
|
||||
<a
|
||||
href=${this.notification.link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click=${stopEventFromActivatingCardWideActions}
|
||||
>${this.notification.link.title}</a
|
||||
>
|
||||
</div>`
|
||||
: ''}
|
||||
</div>
|
||||
${controls.length
|
||||
? html`<div class="controls">
|
||||
${controls.map((control) => this._renderControl(control))}
|
||||
</div>`
|
||||
: ''}
|
||||
<div class="close" @click=${this._dismiss}>
|
||||
<advanced-camera-card-icon
|
||||
.icon=${{ icon: 'mdi:close' }}
|
||||
></advanced-camera-card-icon>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderControl(control: NotificationControl): TemplateResult {
|
||||
const severityClass = control.severity ? `severity-${control.severity}` : '';
|
||||
return html`
|
||||
<div
|
||||
class="control ${severityClass}"
|
||||
title=${control.tooltip ?? ''}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(control.actions?.hold_action),
|
||||
hasDoubleClick: hasAction(control.actions?.double_tap_action),
|
||||
})}
|
||||
@action=${(ev: CustomEvent) => this._handleControlAction(ev, control)}
|
||||
>
|
||||
${control.icon
|
||||
? html`<advanced-camera-card-icon
|
||||
.icon=${{ icon: control.icon }}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleControlAction(
|
||||
ev: CustomEvent<{ action: string }>,
|
||||
control: NotificationControl,
|
||||
): void {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
|
||||
const action = getActionConfigGivenAction(ev.detail.action, control.actions);
|
||||
if (action) {
|
||||
dispatchActionExecutionRequest(this, {
|
||||
actions: arrayify(action),
|
||||
});
|
||||
}
|
||||
if (control.dismiss !== false) {
|
||||
this._dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderDetail(detail: NotificationDetail, isHeading = false): TemplateResult {
|
||||
const classes = {
|
||||
detail: true,
|
||||
heading: isHeading,
|
||||
[`severity-${detail.severity}`]: !!detail.severity,
|
||||
};
|
||||
return html`
|
||||
<div class="${classMap(classes)}">
|
||||
${detail.icon
|
||||
? html`<advanced-camera-card-icon
|
||||
title=${detail.tooltip ?? ''}
|
||||
.icon=${{ icon: detail.icon }}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
<span title=${detail.text}>${detail.text}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dismiss = (): void => {
|
||||
this._refNotification.value?.classList.add('exiting');
|
||||
};
|
||||
|
||||
private _handleAnimationEnd = (ev: AnimationEvent): void => {
|
||||
if (ev.animationName === 'slideDown') {
|
||||
dispatchDismissNotificationEvent(this);
|
||||
}
|
||||
};
|
||||
|
||||
private _handleOutsideInteraction = (ev: Event): void => {
|
||||
if (!ev.composedPath().includes(this)) {
|
||||
this._dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
private _handleKeyDown = (ev: KeyboardEvent): void => {
|
||||
if (ev.key === 'Escape') {
|
||||
this._dismiss();
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(notificationStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-notification': AdvancedCameraCardNotification;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { handleControlAction } from '../../components-lib/notification/action.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import {
|
||||
createNotificationFromText,
|
||||
NotificationOptions,
|
||||
} from '../../components-lib/notification/factory.js';
|
||||
import { Notification } from '../../config/schema/actions/types.js';
|
||||
import notificationBlockStyle from '../../scss/notification-block.scss';
|
||||
import {
|
||||
renderControl,
|
||||
renderDetail,
|
||||
renderNotificationBody,
|
||||
} from './common-rendering.js';
|
||||
|
||||
export function renderNotificationBlock(
|
||||
notification: Notification | null,
|
||||
): TemplateResult {
|
||||
return html`<advanced-camera-card-notification-block
|
||||
.notification=${notification}
|
||||
></advanced-camera-card-notification-block>`;
|
||||
}
|
||||
|
||||
export function renderNotificationBlockFromText(
|
||||
text: string,
|
||||
options?: NotificationOptions,
|
||||
): TemplateResult {
|
||||
return renderNotificationBlock(createNotificationFromText(text, options));
|
||||
}
|
||||
|
||||
@customElement('advanced-camera-card-notification-block')
|
||||
export class AdvancedCameraCardNotificationBlock extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public notification: Notification | null = null;
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { heading, in_progress } = this.notification;
|
||||
const controls = this.notification.controls ?? [];
|
||||
|
||||
// Anchor the spinner to whichever status element is most prominent: the
|
||||
// heading if present, the body icon otherwise. Never render it orphaned
|
||||
// on its own row (which would float it to the left with no visual tie to
|
||||
// the text).
|
||||
const spinner = in_progress
|
||||
? html`<div class="spinner" title=${localize('common.in_progress')}>
|
||||
<ha-spinner indeterminate size="tiny"></ha-spinner>
|
||||
</div>`
|
||||
: null;
|
||||
const spinnerInHeadingRow = spinner && (heading || controls.length);
|
||||
const spinnerInBody = spinner && !spinnerInHeadingRow;
|
||||
|
||||
return html`
|
||||
<div class="content">
|
||||
${heading || spinnerInHeadingRow || controls.length
|
||||
? html`<div class="heading-row">
|
||||
${heading ? renderDetail(heading, 'heading') : ''}
|
||||
${spinnerInHeadingRow || controls.length
|
||||
? html`<div class="controls">
|
||||
${spinnerInHeadingRow ? spinner : ''}
|
||||
${controls.map((control) =>
|
||||
renderControl(control, (ev, c) =>
|
||||
handleControlAction(ev, c, this),
|
||||
),
|
||||
)}
|
||||
</div>`
|
||||
: ''}
|
||||
</div>`
|
||||
: ''}
|
||||
${renderNotificationBody(this.notification, spinnerInBody ? spinner : undefined)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(notificationBlockStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-notification-block': AdvancedCameraCardNotificationBlock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { html, TemplateResult } from 'lit';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { actionHandler } from '../../action-handler-directive.js';
|
||||
import {
|
||||
Notification,
|
||||
NotificationControl,
|
||||
NotificationDetail,
|
||||
} from '../../config/schema/actions/types.js';
|
||||
import {
|
||||
hasAction,
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../../utils/action.js';
|
||||
import '../icon.js';
|
||||
|
||||
export function renderDetail(
|
||||
detail: NotificationDetail,
|
||||
role: 'heading' | 'body' | 'metadata' = 'metadata',
|
||||
iconOverride?: TemplateResult,
|
||||
): TemplateResult {
|
||||
const classes = {
|
||||
detail: true,
|
||||
heading: role === 'heading',
|
||||
body: role === 'body',
|
||||
[`severity-${detail.severity}`]: !!detail.severity,
|
||||
};
|
||||
return html`
|
||||
<div class="${classMap(classes)}">
|
||||
${iconOverride ??
|
||||
(detail.icon
|
||||
? html`<advanced-camera-card-icon
|
||||
title=${detail.tooltip ?? ''}
|
||||
.icon=${{ icon: detail.icon }}
|
||||
></advanced-camera-card-icon>`
|
||||
: '')}
|
||||
<span title=${detail.text}>${detail.text}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderControl(
|
||||
control: NotificationControl,
|
||||
onAction: (ev: CustomEvent<{ action: string }>, control: NotificationControl) => void,
|
||||
): TemplateResult {
|
||||
const classes = {
|
||||
control: true,
|
||||
[`severity-${control.severity}`]: !!control.severity,
|
||||
};
|
||||
return html`
|
||||
<div
|
||||
class="${classMap(classes)}"
|
||||
title=${control.tooltip ?? ''}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: hasAction(control.actions?.hold_action),
|
||||
hasDoubleClick: hasAction(control.actions?.double_tap_action),
|
||||
})}
|
||||
@action=${(ev: CustomEvent) => onAction(ev, control)}
|
||||
>
|
||||
${control.icon
|
||||
? html`<advanced-camera-card-icon
|
||||
.icon=${{ icon: control.icon }}
|
||||
></advanced-camera-card-icon>`
|
||||
: ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
export function renderNotificationBody(
|
||||
notification: Notification,
|
||||
bodyIconOverride?: TemplateResult,
|
||||
): TemplateResult {
|
||||
const { body, link } = notification;
|
||||
const context = notification.context ?? [];
|
||||
const metadata = notification.metadata ?? [];
|
||||
return html`
|
||||
${metadata.map((detail) => renderDetail(detail, 'metadata'))}
|
||||
${body ? renderDetail(body, 'body', bodyIconOverride) : ''}
|
||||
${link
|
||||
? html`<div class="url">
|
||||
<a
|
||||
href=${link.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@click=${stopEventFromActivatingCardWideActions}
|
||||
>${link.title}</a
|
||||
>
|
||||
</div>`
|
||||
: ''}
|
||||
${context.length
|
||||
? html`<div class="context">
|
||||
${context.map((item) => html`<pre>${item}</pre>`)}
|
||||
</div>`
|
||||
: ''}
|
||||
`;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { TemplateResult } from 'lit';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import { renderNotificationBlock } from './block.js';
|
||||
|
||||
interface NoMediaOptions {
|
||||
cameraID: string | null;
|
||||
cameraManager: CameraManager | null;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function renderNoMedia(options: NoMediaOptions): TemplateResult {
|
||||
const cameraID =
|
||||
options.cameraID ?? options.cameraManager?.getStore().getDefaultCameraID() ?? null;
|
||||
const cameraTitle = cameraID
|
||||
? options.cameraManager?.getCameraMetadata(cameraID)?.title ?? cameraID
|
||||
: null;
|
||||
|
||||
return renderNotificationBlock({
|
||||
heading: {
|
||||
text: options.loading
|
||||
? localize('error.awaiting_media')
|
||||
: localize('common.no_media'),
|
||||
icon: 'mdi:multimedia',
|
||||
},
|
||||
in_progress: options.loading,
|
||||
...(cameraTitle && {
|
||||
metadata: [{ text: cameraTitle, icon: 'mdi:cctv' }],
|
||||
}),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { handleControlAction } from '../../components-lib/notification/action.js';
|
||||
import { Notification } from '../../config/schema/actions/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import notificationPopupStyle from '../../scss/notification-popup.scss';
|
||||
import { dispatchDismissNotificationEvent } from '../../utils/notification.js';
|
||||
import {
|
||||
renderControl,
|
||||
renderDetail,
|
||||
renderNotificationBody,
|
||||
} from './common-rendering.js';
|
||||
|
||||
@customElement('advanced-camera-card-notification')
|
||||
export class AdvancedCameraCardNotification extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public notification: Notification | null = null;
|
||||
|
||||
private _refNotification: Ref<HTMLElement> = createRef();
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener('click', this._handleOutsideInteraction);
|
||||
window.addEventListener('focusin', this._handleOutsideInteraction);
|
||||
window.addEventListener('keydown', this._handleKeyDown);
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
window.removeEventListener('click', this._handleOutsideInteraction);
|
||||
window.removeEventListener('focusin', this._handleOutsideInteraction);
|
||||
window.removeEventListener('keydown', this._handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.notification) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { heading, in_progress } = this.notification;
|
||||
const controls = this.notification.controls ?? [];
|
||||
|
||||
return html`
|
||||
<div class="backdrop" @click=${this._dismiss}></div>
|
||||
<div
|
||||
class="notification"
|
||||
${ref(this._refNotification)}
|
||||
@animationend=${this._handleAnimationEnd}
|
||||
>
|
||||
${controls.length || in_progress
|
||||
? html`<div class="controls">
|
||||
${in_progress
|
||||
? html`<div class="spinner" title=${localize('common.in_progress')}>
|
||||
<ha-spinner indeterminate size="tiny"></ha-spinner>
|
||||
</div>`
|
||||
: ''}
|
||||
${controls.map((control) =>
|
||||
renderControl(control, (ev, c) =>
|
||||
handleControlAction(ev, c, this, this._dismiss),
|
||||
),
|
||||
)}
|
||||
</div>`
|
||||
: ''}
|
||||
<div class="close" @click=${this._dismiss}>
|
||||
<advanced-camera-card-icon
|
||||
.icon=${{ icon: 'mdi:close' }}
|
||||
></advanced-camera-card-icon>
|
||||
</div>
|
||||
<div class="details">
|
||||
${heading ? renderDetail(heading, 'heading') : ''}
|
||||
${renderNotificationBody(this.notification)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _dismiss = (): void => {
|
||||
this._refNotification.value?.classList.add('exiting');
|
||||
};
|
||||
|
||||
private _handleAnimationEnd = (ev: AnimationEvent): void => {
|
||||
if (ev.animationName === 'slideDown') {
|
||||
dispatchDismissNotificationEvent(this);
|
||||
}
|
||||
};
|
||||
|
||||
private _handleOutsideInteraction = (ev: Event): void => {
|
||||
if (!ev.composedPath().includes(this)) {
|
||||
this._dismiss();
|
||||
}
|
||||
};
|
||||
|
||||
private _handleKeyDown = (ev: KeyboardEvent): void => {
|
||||
if (ev.key === 'Escape') {
|
||||
this._dismiss();
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(notificationPopupStyle);
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'advanced-camera-card-notification': AdvancedCameraCardNotification;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||
import { ref, Ref } from 'lit/directives/ref.js';
|
||||
import { CardWideConfig } from '../config/schema/types';
|
||||
import messageStyle from '../scss/message.scss';
|
||||
import progressIndicatorStyle from '../scss/progress-indicator.scss';
|
||||
import './icon.js';
|
||||
|
||||
type AdvancedCameraCardProgressIndicatorSize = 'tiny' | 'small' | 'medium' | 'large';
|
||||
@@ -40,7 +40,7 @@ export class AdvancedCameraCardProgressIndicator extends LitElement {
|
||||
public size: AdvancedCameraCardProgressIndicatorSize = 'large';
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html` <div class="message vertical">
|
||||
return html` <div class="indicator">
|
||||
${this.animated
|
||||
? html`<ha-spinner indeterminate size="${this.size}"> </ha-spinner>`
|
||||
: html`<advanced-camera-card-icon
|
||||
@@ -51,7 +51,7 @@ export class AdvancedCameraCardProgressIndicator extends LitElement {
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return unsafeCSS(messageStyle);
|
||||
return unsafeCSS(progressIndicatorStyle);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { CameraManager } from '../../camera-manager/manager';
|
||||
import { MediaDetailsController } from '../../components-lib/media/details-controller';
|
||||
import { MediaNotificationController } from '../../components-lib/media/notification-controller';
|
||||
import { NotificationDetail } from '../../config/schema/actions/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import thumbnailDetailsStyle from '../../scss/thumbnail-details.scss';
|
||||
@@ -31,17 +31,17 @@ export class AdvancedCameraCardThumbnailDetails extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public seek?: Date;
|
||||
|
||||
private _controller = new MediaDetailsController();
|
||||
private _notificationController = new MediaNotificationController();
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (['item', 'seek', 'cameraManager'].some((prop) => changedProperties.has(prop))) {
|
||||
this._controller.calculate(this.cameraManager, this.item, this.seek);
|
||||
this._notificationController.calculate(this.cameraManager, this.item, this.seek);
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
const heading = this._controller.getHeading();
|
||||
const details = this._controller.getDetails();
|
||||
const heading = this._notificationController.getHeading();
|
||||
const details = this._notificationController.getMetadata();
|
||||
|
||||
const renderDetail = (
|
||||
detail: NotificationDetail,
|
||||
|
||||
@@ -13,9 +13,9 @@ import { dispatchActionExecutionRequest } from '../../../card-controller/actions
|
||||
import { ViewItemManager } from '../../../card-controller/view/item-manager';
|
||||
import { ViewManagerEpoch } from '../../../card-controller/view/types';
|
||||
import {
|
||||
MediaDetailsController,
|
||||
MediaNotificationController,
|
||||
NotificationControlsContext,
|
||||
} from '../../../components-lib/media/details-controller';
|
||||
} from '../../../components-lib/media/notification-controller';
|
||||
import { ThumbnailFeatureController } from '../../../components-lib/thumbnail/feature/controller';
|
||||
import { HomeAssistant } from '../../../ha/types';
|
||||
import { localize } from '../../../localize/localize';
|
||||
@@ -208,12 +208,12 @@ export class AdvancedCameraCardThumbnailFeature extends LitElement {
|
||||
title=${this.item?.getDescription() ?? ''}
|
||||
@click=${(ev: Event) => {
|
||||
stopEventFromActivatingCardWideActions(ev);
|
||||
const detailsController = new MediaDetailsController();
|
||||
detailsController.calculate(this.cameraManager, this.item);
|
||||
const notificationController = new MediaNotificationController();
|
||||
notificationController.calculate(this.cameraManager, this.item);
|
||||
dispatchActionExecutionRequest(this, {
|
||||
actions: [
|
||||
createNotificationAction(
|
||||
detailsController.getNotification(this._getControlContext()),
|
||||
notificationController.getNotification(this._getControlContext()),
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ import { contentsChanged } from '../utils/basic';
|
||||
import './date-picker.js';
|
||||
import { AdvancedCameraCardDatePicker, DatePickerEvent } from './date-picker.js';
|
||||
import './icon';
|
||||
import { renderMessage } from './message';
|
||||
import { renderNotificationBlockFromText } from './notification/block';
|
||||
import './thumbnail/thumbnail.js';
|
||||
|
||||
/**
|
||||
@@ -151,11 +151,9 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
|
||||
if (isLoading) {
|
||||
if (!this.mini) {
|
||||
return renderMessage({
|
||||
message: localize('error.awaiting_media'),
|
||||
return renderNotificationBlockFromText(localize('error.awaiting_media'), {
|
||||
icon: 'mdi:chart-gantt',
|
||||
type: 'info',
|
||||
dotdotdot: true,
|
||||
in_progress: true,
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -163,11 +161,10 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
|
||||
if (!view?.query || !view.query.hasNodes()) {
|
||||
if (!this.mini) {
|
||||
return renderMessage({
|
||||
message: localize('error.no_camera_or_media_for_timeline'),
|
||||
icon: 'mdi:chart-gantt',
|
||||
type: 'info',
|
||||
});
|
||||
return renderNotificationBlockFromText(
|
||||
localize('error.no_camera_or_media_for_timeline'),
|
||||
{ icon: 'mdi:chart-gantt' },
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { guard } from 'lit/directives/guard.js';
|
||||
import { keyed } from 'lit/directives/keyed.js';
|
||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||
import { CameraManager } from '../../camera-manager/manager.js';
|
||||
import { RemoveContextPropertyViewModifier } from '../../card-controller/view/modifiers/remove-context-property.js';
|
||||
@@ -32,8 +33,8 @@ import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
import { ViewMedia } from '../../view/item.js';
|
||||
import '../carousel';
|
||||
import type { EmblaCarouselPlugins } from '../carousel.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import '../next-prev-control.js';
|
||||
import { renderNoMedia } from '../notification/no-media.js';
|
||||
import '../ptz.js';
|
||||
import './provider.js';
|
||||
|
||||
@@ -314,15 +315,12 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
protected render(): TemplateResult | void {
|
||||
const mediaCount = this._media?.length ?? 0;
|
||||
if (!this._media || !mediaCount) {
|
||||
return renderMessage({
|
||||
message: localize('common.no_media'),
|
||||
type: 'info',
|
||||
icon: 'mdi:multimedia',
|
||||
...(this.viewFilterCameraID && {
|
||||
context: {
|
||||
camera_id: this.viewFilterCameraID,
|
||||
},
|
||||
}),
|
||||
return renderNoMedia({
|
||||
cameraID:
|
||||
this.viewFilterCameraID ??
|
||||
this.viewManagerEpoch?.manager.getView()?.camera ??
|
||||
null,
|
||||
cameraManager: this.cameraManager ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -464,16 +462,22 @@ export class AdvancedCameraCardViewerCarousel extends LitElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
const mediaID = media.getID();
|
||||
const mediaEpoch = mediaID ? view.context?.mediaEpoch?.[mediaID] ?? 0 : 0;
|
||||
|
||||
return html` <div class="embla__slide">
|
||||
<advanced-camera-card-viewer-provider
|
||||
.hass=${this.hass}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.media=${media}
|
||||
.viewerConfig=${this.viewerConfig}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
></advanced-camera-card-viewer-provider>
|
||||
${keyed(
|
||||
mediaEpoch,
|
||||
html`<advanced-camera-card-viewer-provider
|
||||
.hass=${this.hass}
|
||||
.viewManagerEpoch=${this.viewManagerEpoch}
|
||||
.media=${media}
|
||||
.viewerConfig=${this.viewerConfig}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
></advanced-camera-card-viewer-provider>`,
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,10 @@ import { CardWideConfig } from '../../config/schema/types.js';
|
||||
import { ViewerConfig } from '../../config/schema/viewer.js';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import '../../patches/ha-hls-player.js';
|
||||
import viewerStyle from '../../scss/viewer.scss';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import { renderNoMedia } from '../notification/no-media.js';
|
||||
import './grid';
|
||||
|
||||
export interface MediaViewerViewContext {
|
||||
@@ -81,15 +80,10 @@ export class AdvancedCameraCardViewer extends LitElement {
|
||||
// Directly render an error message (instead of dispatching it upwards)
|
||||
// to preserve the mini-timeline if the user pans into an area with no
|
||||
// media.
|
||||
const loadingMedia =
|
||||
!!this.viewManagerEpoch.manager.getView()?.context?.loading?.query;
|
||||
return renderMessage({
|
||||
type: 'info',
|
||||
message: loadingMedia
|
||||
? localize('error.awaiting_media')
|
||||
: localize('common.no_media'),
|
||||
icon: 'mdi:multimedia',
|
||||
dotdotdot: loadingMedia,
|
||||
return renderNoMedia({
|
||||
cameraID: this.viewManagerEpoch.manager.getView()?.camera ?? null,
|
||||
cameraManager: this.cameraManager ?? null,
|
||||
loading: !!this.viewManagerEpoch.manager.getView()?.context?.loading?.query,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import { ViewItemClassifier } from '../../view/item-classifier.js';
|
||||
import { VideoContentType, ViewMedia } from '../../view/item.js';
|
||||
import { UnifiedQueryTransformer } from '../../view/unified-query-transformer.js';
|
||||
import '../image-player.js';
|
||||
import { renderMessage } from '../message.js';
|
||||
import { renderNotificationBlockFromText } from '../notification/block.js';
|
||||
import { renderProgressIndicator } from '../progress-indicator.js';
|
||||
import '../video-player.js';
|
||||
import './../media-dimensions-container';
|
||||
@@ -233,13 +233,13 @@ export class AdvancedCameraCardViewerProvider extends LitElement implements Medi
|
||||
|
||||
const error = this._signedURLController.getError();
|
||||
if (error) {
|
||||
return renderMessage({
|
||||
type: 'error',
|
||||
message: localize(
|
||||
error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign',
|
||||
),
|
||||
context: this.media?.getContentID(),
|
||||
});
|
||||
const contentID = this.media?.getContentID();
|
||||
return renderNotificationBlockFromText(
|
||||
localize(error === 'proxy' ? 'error.failed_proxy' : 'error.failed_sign'),
|
||||
{
|
||||
...(contentID && { metadata: [{ text: contentID, icon: 'mdi:identifier' }] }),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const url = this._signedURLController.getValue();
|
||||
|
||||
@@ -10,7 +10,7 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { FoldersManager } from '../card-controller/folders/manager.js';
|
||||
import { ProblemPresence } from '../card-controller/problems/types.js';
|
||||
import { IssuePresence } from '../card-controller/issues/types.js';
|
||||
import { MicrophoneState } from '../card-controller/types.js';
|
||||
import { ViewItemManager } from '../card-controller/view/item-manager.js';
|
||||
import { ViewManagerEpoch } from '../card-controller/view/types.js';
|
||||
@@ -70,7 +70,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
public deviceRegistryManager?: DeviceRegistryManager;
|
||||
|
||||
@property({ attribute: false })
|
||||
public problems?: ProblemPresence;
|
||||
public issues?: IssuePresence;
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionStateManager?: ConditionStateManagerReadonlyInterface;
|
||||
@@ -215,7 +215,7 @@ export class AdvancedCameraCardViews extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.rawConfig=${this.rawConfig}
|
||||
.deviceRegistryManager=${this.deviceRegistryManager}
|
||||
.problems=${this.problems}
|
||||
.issues=${this.issues}
|
||||
>
|
||||
</advanced-camera-card-diagnostics>`
|
||||
: ``}
|
||||
|
||||
Reference in New Issue
Block a user