diff --git a/src/action-handler-directive.ts b/src/action-handler-directive.ts index 25fcd421..b381996d 100644 --- a/src/action-handler-directive.ts +++ b/src/action-handler-directive.ts @@ -1,17 +1,16 @@ +import { fireEvent } from 'custom-card-helpers'; +import type { + ActionHandlerDetail, + ActionHandlerOptions +} from 'custom-card-helpers/dist/types.d.js'; import { noChange } from 'lit'; import { AttributePart, directive, Directive, - DirectiveParameters, + DirectiveParameters } from 'lit/directive.js'; - -import type { - ActionHandlerDetail, - ActionHandlerOptions, -} from 'custom-card-helpers/dist/types.d.js'; -import { fireEvent } from 'custom-card-helpers'; -import { stopEventFromActivatingCardWideActions } from './common'; +import { stopEventFromActivatingCardWideActions } from './utils/action.js'; interface ActionHandler extends HTMLElement { holdTime: number; diff --git a/src/browse-media-util.ts b/src/browse-media-util.ts deleted file mode 100644 index aa58e3ad..00000000 --- a/src/browse-media-util.ts +++ /dev/null @@ -1,415 +0,0 @@ -import { HomeAssistant } from 'custom-card-helpers'; - -import { - BrowseMediaQueryParameters, - FrigateBrowseMediaSource, - CameraConfig, - MEDIA_CLASS_PLAYLIST, - MEDIA_TYPE_PLAYLIST, -} from './types.js'; -import { View } from './view.js'; -import { frigateBrowseMediaSourceSchema } from './types.js'; -import { - dispatchErrorMessageEvent, - dispatchMessageEvent, - getCameraTitle, - homeAssistantWSRequest, -} from './common.js'; -import { localize } from './localize/localize.js'; - -export class BrowseMediaUtil { - /** - * Return the Frigate event_id given a FrigateBrowseMediaSource object. - * @param media The event to get the id from. - * @returns The `event_id` or `null` if not successfully parsed. - */ - static getEventID(media: FrigateBrowseMediaSource): string | null { - return media.frigate?.event.id ?? null; - } - - /** - * Return the event start time given a FrigateBrowseMediaSource object. - * @param browseMedia The media object to get the start time from. - * @returns The start time in unix/epoch time, or null if it cannot be determined. - */ - static getEventStartTime(media: FrigateBrowseMediaSource): number | null { - return media.frigate?.event.start_time ?? null; - } - - /** - * Determine if a FrigateBrowseMediaSource object is truly a media item (vs a folder). - * @param media The media object. - * @returns `true` if it's truly a media item, `false` otherwise. - */ - static isTrueMedia(media?: FrigateBrowseMediaSource): boolean { - return !!media && !media.can_expand; - } - - /** - * From a FrigateBrowseMediaSource item extract the first true media item from the - * children (i.e. a clip/snapshot, not a folder). - * @param media The media object with children. - * @returns The first true media item found. - */ - static getFirstTrueMediaChildIndex( - media: FrigateBrowseMediaSource | null, - ): number | null { - if (!media || !media.children) { - return null; - } - const index = media.children.findIndex((child) => this.isTrueMedia(child)); - return index >= 0 ? index : null; - } - - /** - * Browse Frigate media with a media content id. May throw. - * @param hass The HomeAssistant object. - * @param media_content_id The media content id to browse. - * @returns A FrigateBrowseMediaSource object or null on malformed. - */ - static async browseMedia( - hass: HomeAssistant, - media_content_id: string, - ): Promise { - const request = { - type: 'media_source/browse_media', - media_content_id: media_content_id, - }; - return await homeAssistantWSRequest(hass, frigateBrowseMediaSourceSchema, request); - } - - /** - * Browse Frigate media with a media query. May throw. - * @param hass The HomeAssistant object. - * @param params The search parameters to use to search for media. - * @returns A FrigateBrowseMediaSource object or null on malformed. - */ - static async browseMediaQuery( - hass: HomeAssistant, - params: BrowseMediaQueryParameters, - ): Promise { - return this.browseMedia( - hass, - // Defined in: - // https://github.com/blakeblackshear/frigate-hass-integration/blob/master/custom_components/frigate/media_source.py - [ - 'media-source://frigate', - params.clientId, - 'event-search', - params.mediaType, - - // If the name field ends in '.all' the integration will return up to 10K events. - params.unlimited ? '.all' : '', - params.after ? String(Math.floor(params.after)) : '', - params.before ? String(Math.ceil(params.before)) : '', - params.cameraName, - params.label, - params.zone, - ].join('/'), - ); - } - - /** - * Browse multiple Frigate media queries. May throw. - * @param hass The HomeAssistant object. - * @param params An array of search parameters to use to search for media. - * @returns A map of FrigateBrowseMediaSource object or null on malformed. - */ - static async multipleBrowseMediaQuery( - hass: HomeAssistant, - params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[], - ): Promise> { - params = Array.isArray(params) ? params : [params]; - const output: Map = new Map(); - await Promise.all( - params.map(async (param: BrowseMediaQueryParameters): Promise => { - output.set(param, await this.browseMediaQuery(hass, param)); - }), - ); - return output; - } - - /** - * Browse multiple Frigate media queries, then merged them. May throw. - * @param hass The HomeAssistant object. - * @param params An array of search parameters to use to search for media. - * @returns A single FrigateBrowseMediaSource object or null on malformed. - */ - static async multipleBrowseMediaQueryMerged( - hass: HomeAssistant, - params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[], - ): Promise { - return this.mergeFrigateBrowseMediaSources( - await this.multipleBrowseMediaQuery(hass, params), - ); - } - - /** - * Merge multiple FrigateBrowseMediaSource into a single. Note that this may - * use information from the query to differentiate results that may otherwise - * be identical. - * @param input A map of query -> result. - * @returns A single FrigateBrowseMediaSource object. - */ - static mergeFrigateBrowseMediaSources( - input: Map, - ): FrigateBrowseMediaSource { - const children: FrigateBrowseMediaSource[] = []; - - for (const [query, result] of input.entries()) { - for (const child of result.children || []) { - if (this.isTrueMedia(child)) { - children.push(child); - } else { - // If there are multiple inputs, separate the folder names with the - // query title (if available). - if (query.title && input.size > 1) { - children.push({ ...child, title: `[${query.title}] ${child.title}` }); - } else { - children.push(child); - } - } - } - } - - const eventSort = ( - a: FrigateBrowseMediaSource, - b: FrigateBrowseMediaSource, - ): number => { - if ( - !a.frigate?.event || - (b.frigate?.event && b.frigate.event.start_time > a.frigate.event.start_time) - ) { - return 1; - } - - if ( - !b.frigate?.event || - (a.frigate?.event && b.frigate.event.start_time < a.frigate.event.start_time) - ) { - return -1; - } - return 0; - }; - - return this.createEventParentForChildren('Merged events', children.sort(eventSort)); - } - - /** - * Get the parameters to search for media. - * @returns A BrowseMediaQueryParameters object. - */ - static getBrowseMediaQueryParameters( - hass: HomeAssistant, - cameraID: string, - cameraConfig?: CameraConfig, - overrides?: Partial, - ): BrowseMediaQueryParameters | null { - if (!cameraConfig || !cameraConfig.camera_name) { - return null; - } - return { - clientId: cameraConfig.client_id, - cameraName: cameraConfig.camera_name, - label: cameraConfig.label, - zone: cameraConfig.zone, - title: getCameraTitle(hass, cameraConfig), - cameraID: cameraID, - ...overrides, - }; - } - - /** - * Apply overrides to multiple query parameters. - * @param parameters An array of query parameters. - * @param overrides The overrides to apply. - * @returns The override query parameters. - */ - static overrideMultiBrowseMediaQueryParameters( - parameters: BrowseMediaQueryParameters[], - overrides: Partial, - ): BrowseMediaQueryParameters[] { - const output: BrowseMediaQueryParameters[] = []; - parameters.forEach((param) => { - output.push({ ...param, ...overrides }); - }); - return output; - } - - /** - * Get BrowseMediaQueryParameters for a camera (including its dependencies). - * @param hass Home Assistant object. - * @param cameras Cameras map. - * @param camera Name of the current camera. - * @param mediaType Optional media type to include in the parameters. - * @returns An array of query parameters. - */ - static getFullDependentBrowseMediaQueryParameters( - hass: HomeAssistant, - cameras: Map, - camera: string, - mediaType?: 'clips' | 'snapshots', - ): BrowseMediaQueryParameters[] | null { - const cameraIDs: Set = new Set(); - const getDependentCameras = (camera: string): void => { - const cameraConfig = cameras.get(camera); - if (cameraConfig) { - cameraIDs.add(camera); - for (const eventCameraID of cameraConfig.dependent_cameras || []) { - if (!cameraIDs.has(eventCameraID)) { - getDependentCameras(eventCameraID); - } - } - } - }; - getDependentCameras(camera); - - const params: BrowseMediaQueryParameters[] = []; - for (const cameraID of cameraIDs) { - const param = BrowseMediaUtil.getBrowseMediaQueryParameters( - hass, - cameraID, - cameras.get(cameraID), - mediaType ? { mediaType: mediaType } : {}, - ); - // Fail on a single bad camera, as it's almost certainly a user error that - // should be fixed rather than hidden. - if (!param) { - return null; - } - params.push(param); - } - return params.length ? params : null; - } - - /** - * Get BrowseMediaQueryParameters for a camera (including its dependencies) or dispatch an error. - * @param element The element from which to dispatch the error. - * @param hass Home Assistant object. - * @param cameras Cameras map. - * @param camera Name of the current camera. - * @param mediaType Optional media type to include in the parameters. - * @returns An array of query parameters. - */ - static getFullDependentBrowseMediaQueryParametersOrDispatchError( - element: HTMLElement, - hass: HomeAssistant, - cameras: Map, - camera: string, - mediaType?: 'clips' | 'snapshots', - ): BrowseMediaQueryParameters[] | null { - const params = this.getFullDependentBrowseMediaQueryParameters( - hass, - cameras, - camera, - mediaType, - ); - if (!params) { - dispatchErrorMessageEvent( - element, - localize('error.no_camera_name'), - cameras.get(camera), - ); - return null; - } - return params; - } - - /** - * Fetch the latest media and dispatch a change view event to reflect the - * results. If no media is found a suitable message event will be triggered - * instead. - * @param element The HTMLElement to dispatch events from. - * @param hass The Home Assistant object. - * @param view The current view to evolve. - * @param browseMediaQueryParameters The media parameters to query with. - * @returns - */ - static async fetchLatestMediaAndDispatchViewChange( - element: HTMLElement, - hass: HomeAssistant, - view: Readonly, - browseMediaQueryParameters: - | BrowseMediaQueryParameters - | BrowseMediaQueryParameters[], - ): Promise { - let parent: FrigateBrowseMediaSource | null; - try { - parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged( - hass, - browseMediaQueryParameters, - ); - } catch (e) { - return dispatchErrorMessageEvent(element, (e as Error).message); - } - const childIndex = BrowseMediaUtil.getFirstTrueMediaChildIndex(parent); - if (!parent || !parent.children || childIndex == null) { - return dispatchMessageEvent( - element, - view.isClipRelatedView() - ? localize('common.no_clip') - : localize('common.no_snapshot'), - view.isClipRelatedView() ? 'mdi:filmstrip-off' : 'mdi:camera-off', - ); - } - - view - .evolve({ - target: parent, - childIndex: childIndex, - }) - .dispatchChangeEvent(element); - } - - /** - * Fetch the media of a child FrigateBrowseMediaSource object and dispatch a change - * view event to reflect the results. - * @param node The HTMLElement to dispatch events from. - * @param hass The Home Assistant object. - * @param view The current view to evolve. - * @param child The FrigateBrowseMediaSource child to query for. - * @returns - */ - static async fetchChildMediaAndDispatchViewChange( - element: HTMLElement, - hass: HomeAssistant, - view: Readonly, - child: Readonly, - ): Promise { - let parent: FrigateBrowseMediaSource; - try { - parent = await BrowseMediaUtil.browseMedia(hass, child.media_content_id); - } catch (e) { - return dispatchErrorMessageEvent(element, (e as Error).message); - } - - view - .evolve({ - target: parent, - }) - .dispatchChangeEvent(element); - } - - /** - * Given an array of media children, create a parent for them. - * @param title The title to use for the parent. - * @param children The children media items. - * @returns A single parent containing the children. - */ - static createEventParentForChildren( - title: string, - children: FrigateBrowseMediaSource[], - ): FrigateBrowseMediaSource { - return { - title: title, - media_class: MEDIA_CLASS_PLAYLIST, - media_content_type: MEDIA_TYPE_PLAYLIST, - media_content_id: '', - can_play: false, - can_expand: true, - children_media_class: MEDIA_CLASS_PLAYLIST, - thumbnail: null, - children: children, - }; - } -} diff --git a/src/card.ts b/src/card.ts index 8d33002b..59aaa443 100644 --- a/src/card.ts +++ b/src/card.ts @@ -1,98 +1,94 @@ +import { getLovelace, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers'; import { CSSResultGroup, + html, LitElement, PropertyValues, TemplateResult, - html, - unsafeCSS, + unsafeCSS } from 'lit'; -import { HomeAssistant, LovelaceCardEditor, getLovelace } from 'custom-card-helpers'; -import { StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { customElement, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { createRef, ref, Ref } from 'lit/directives/ref.js'; -import screenfull from 'screenfull'; -import { throttle } from 'lodash-es'; +import { StyleInfo, styleMap } from 'lit/directives/style-map.js'; import { until } from 'lit/directives/until.js'; +import { throttle } from 'lodash-es'; +import screenfull from 'screenfull'; import { z } from 'zod'; - +import { actionHandler } from './action-handler-directive.js'; import { - Actions, - ActionType, - CameraConfig, - FrigateCardView, - FrigateCardCustomAction, - FRIGATE_CARD_VIEWS_USER_SPECIFIED, - RawFrigateCardConfig, - entitySchema, - frigateCardConfigSchema, -} from './types.js'; + ConditionState, + conditionStateRequestHandler, + getOverriddenConfig, + getOverridesByKey +} from './card-condition.js'; +import './components/elements.js'; +import { FrigateCardElements } from './components/elements.js'; +import './components/gallery.js'; +import './components/image.js'; +import { FrigateCardImage } from './components/image.js'; +import './components/live.js'; +import './components/menu.js'; +import { FrigateCardMenu, FRIGATE_BUTTON_MENU_ICON } from './components/menu.js'; +import './components/message.js'; +import { renderMessage, renderProgressIndicator } from './components/message.js'; +import './components/thumbnail-carousel.js'; +import './components/timeline.js'; +import './components/viewer.js'; +import { isConfigUpgradeable } from './config-mgmt.js'; +import { + CAMERA_BIRDSEYE, + CARD_VERSION, + MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, + REPO_URL +} from './const.js'; +import './editor.js'; +import { localize } from './localize/localize.js'; +import './patches/ha-camera-stream.js'; +import './patches/ha-hls-player.js'; +import './patches/ha-web-rtc-player.ts'; +import cardStyle from './scss/card.scss'; import type { Entity, ExtendedHomeAssistant, FrigateCardConfig, MediaShowInfo, MenuButton, - Message, + Message } from './types.js'; - import { - CAMERA_BIRDSEYE, - CARD_VERSION, - MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, - REPO_URL, -} from './const.js'; -import { FrigateCardElements } from './components/elements.js'; -import { FrigateCardImage } from './components/image.js'; -import { FRIGATE_BUTTON_MENU_ICON, FrigateCardMenu } from './components/menu.js'; -import { View } from './view.js'; + Actions, + ActionType, + CameraConfig, + entitySchema, + frigateCardConfigSchema, + FrigateCardCustomAction, + FrigateCardView, + FRIGATE_CARD_VIEWS_USER_SPECIFIED, + RawFrigateCardConfig +} from './types.js'; import { - contentsChanged, convertActionToFrigateCardCustomAction, createFrigateCardCustomAction, frigateCardHandleAction, frigateCardHasAction, - getActionConfigGivenAction, - getCameraIcon, - getCameraID, - getCameraTitle, + getActionConfigGivenAction +} from './utils/action.js'; +import { contentsChanged } from './utils/basic.js'; +import { getCameraIcon, getCameraID, getCameraTitle } from './utils/camera.js'; +import { getEntityIcon, getEntityTitle, homeAssistantSignPath, homeAssistantWSRequest, - isValidMediaShowInfo, shouldUpdateBasedOnHass, - sideLoadHomeAssistantElements, -} from './common.js'; -import { localize } from './localize/localize.js'; -import { renderMessage, renderProgressIndicator } from './components/message.js'; - -import './editor.js'; -import './components/elements.js'; -import './components/gallery.js'; -import './components/image.js'; -import './components/live.js'; -import './components/menu.js'; -import './components/message.js'; -import './components/viewer.js'; -import './components/thumbnail-carousel.js'; -import './components/timeline.js'; -import './patches/ha-camera-stream.js'; -import './patches/ha-hls-player.js'; -import './patches/ha-web-rtc-player.ts'; - -import cardStyle from './scss/card.scss'; -import { ResolvedMediaCache } from './resolved-media.js'; -import { BrowseMediaUtil } from './browse-media-util.js'; -import { isConfigUpgradeable } from './config-mgmt.js'; -import { actionHandler } from './action-handler-directive.js'; -import { - ConditionState, - conditionStateRequestHandler, - getOverriddenConfig, - getOverridesByKey, -} from './card-condition.js'; -import { supportsFeature } from './icons/update.js'; + sideLoadHomeAssistantElements +} from './utils/ha'; +import { getEventID } from './utils/ha/browse-media.js'; +import { supportsFeature } from './utils/ha/update.js'; +import { isValidMediaShowInfo } from './utils/media-info.js'; +import { ResolvedMediaCache } from './utils/resolved-media.js'; +import { View } from './view.js'; /** A note on media callbacks: * @@ -919,7 +915,7 @@ export class FrigateCard extends LitElement { }); return; } - const event_id = BrowseMediaUtil.getEventID(this._view.media); + const event_id = getEventID(this._view.media); if (!event_id) { this._setMessageAndUpdate({ message: localize('error.download_no_event_id'), diff --git a/src/common.ts b/src/common.ts deleted file mode 100644 index d3873370..00000000 --- a/src/common.ts +++ /dev/null @@ -1,723 +0,0 @@ -import { HassEntity, MessageBase } from 'home-assistant-js-websocket'; -import { - HomeAssistant, - computeStateDomain, - handleActionConfig, - hasAction, - ActionConfig, -} from 'custom-card-helpers'; -import { StyleInfo } from 'lit/directives/style-map.js'; -import { ZodSchema, z } from 'zod'; -import { - differenceInSeconds, - differenceInMinutes, - differenceInHours, - fromUnixTime, -} from 'date-fns'; -import { isEqual } from 'lodash-es'; -import { localize } from './localize/localize.js'; -import { - Actions, - ActionsConfig, - ActionType, - CameraConfig, - CardHelpers, - ExtendedHomeAssistant, - FrigateCardAction, - FrigateCardCustomAction, - frigateCardCustomActionSchema, - FrigateEvent, - MediaShowInfo, - Message, - RawFrigateCardConfig, - SignedPath, - signedPathSchema, - StateParameters, -} from './types.js'; -import { stateIcon } from './icons/state-icon.js'; - -const MEDIA_INFO_HEIGHT_CUTOFF = 50; -const MEDIA_INFO_WIDTH_CUTOFF = MEDIA_INFO_HEIGHT_CUTOFF; - -/** - * Get the keys that didn't parse from a ZodError. - * @param error The zoderror to extract the keys from. - * @returns An array of error keys. - */ -export function getParseErrorKeys(error: z.ZodError): string[] { - const errors = error.format(); - return Object.keys(errors).filter((v) => !v.startsWith('_')); -} - -/** - * Make a HomeAssistant websocket request. May throw. - * @param hass The HomeAssistant object to send the request with. - * @param schema The expected Zod schema of the response. - * @param request The request to make. - * @returns The parsed valid response or null on malformed. - */ -export async function homeAssistantWSRequest( - hass: HomeAssistant, - schema: ZodSchema, - request: MessageBase, -): Promise { - const response = await hass.callWS(request); - - if (!response) { - const error_message = `${localize('error.empty_response')}: ${JSON.stringify( - request, - )}`; - console.warn(error_message); - throw new Error(error_message); - } - const parseResult = schema.safeParse(response); - if (!parseResult.success) { - const keys = getParseErrorKeys(parseResult.error); - const error_message = - `${localize('error.invalid_response')}: ${JSON.stringify(request)}. ` + - localize('error.invalid_keys') + - `: '${keys}'`; - console.warn(error_message); - throw new Error(error_message); - } - return parseResult.data; -} - -/** - * Request that HA sign a path. May throw. - * @param hass The HomeAssistant object used to request the signature. - * @param path The path to sign. - * @param expires An optional number of seconds to sign the path for. - * @returns The signed URL, or null if the response was malformed. - */ -export async function homeAssistantSignPath( - hass: ExtendedHomeAssistant, - path: string, - expires?: number, -): Promise { - const request = { - type: 'auth/sign_path', - path: path, - expires: expires, - }; - const response = await homeAssistantWSRequest( - hass, - signedPathSchema, - request, - ); - if (!response) { - return null; - } - return hass.hassUrl(response.path); -} - -/** - * Dispatch a Frigate Card event. - * @param element The element to send the event. - * @param name The name of the Frigate card event to send. - * @param detail An optional detail object to attach. - */ -export function dispatchFrigateCardEvent( - target: EventTarget, - name: string, - detail?: T, -): void { - target.dispatchEvent( - new CustomEvent(`frigate-card:${name}`, { - bubbles: true, - composed: true, - detail: detail, - }), - ); -} - -/** - * Create a MediaShowInfo object. - * @param source An event or HTMLElement that should be used as a source. - * @returns A new MediaShowInfo object or null if one could not be created. - */ -export function createMediaShowInfo(source: Event | HTMLElement): MediaShowInfo | null { - let target: HTMLElement | EventTarget; - if (source instanceof Event) { - target = source.composedPath()[0]; - } else { - target = source; - } - - if (target instanceof HTMLImageElement) { - return { - width: (target as HTMLImageElement).naturalWidth, - height: (target as HTMLImageElement).naturalHeight, - }; - } else if (target instanceof HTMLVideoElement) { - return { - width: (target as HTMLVideoElement).videoWidth, - height: (target as HTMLVideoElement).videoHeight, - }; - } else if (target instanceof HTMLCanvasElement) { - return { - width: (target as HTMLCanvasElement).width, - height: (target as HTMLCanvasElement).height, - }; - } - return null; -} - -/** - * Dispatch a Frigate card media show event. - * @param element The element to send the event. - * @param source An event or HTMLElement that should be used as a source. - */ -export function dispatchMediaShowEvent( - element: HTMLElement, - source: Event | HTMLElement, -): void { - const mediaShowInfo = createMediaShowInfo(source); - if (mediaShowInfo) { - dispatchExistingMediaShowInfoAsEvent(element, mediaShowInfo); - } -} - -/** - * Dispatch a pre-existing MediaShowInfo object as an event. - * @param element The element to send the event. - * @param mediaShowInfo The MediaShowInfo object to send. - */ -export function dispatchExistingMediaShowInfoAsEvent( - element: HTMLElement, - mediaShowInfo: MediaShowInfo, -): void { - dispatchFrigateCardEvent(element, 'media-show', mediaShowInfo); -} - -/** - * Dispatch an event with a message to show to the user. - * @param element The element to send the event. - * @param message The message to show. - * @param icon An optional icon to attach to the message. - */ -export function dispatchMessageEvent( - element: HTMLElement, - message: string, - icon?: string, - context?: unknown, -): void { - dispatchFrigateCardEvent(element, 'message', { - message: message, - type: 'info', - icon: icon, - context: context, - }); -} - -/** - * Dispatch an event with an error message to show to the user. - * @param element The element to send the event. - * @param message The message to show. - */ -export function dispatchErrorMessageEvent( - element: HTMLElement, - message: string, - context?: unknown, -): void { - dispatchFrigateCardEvent(element, 'message', { - message: message, - type: 'error', - context: context, - }); -} - -/** - * Determine whether the card should be updated based on Home Assistant changes. - * @param newHass The new HA object. - * @param oldHass The old HA object. - * @param entities The entities to examine for changes. - * @returns A boolean indicating whether or not to allow an update. - */ -export function shouldUpdateBasedOnHass( - newHass: HomeAssistant | undefined | null, - oldHass: HomeAssistant | undefined | null, - entities: string[] | null, -): boolean { - if (!newHass || !entities || !entities.length) { - return false; - } - if (!oldHass) { - return true; - } - - for (let i = 0; i < entities.length; i++) { - const entity = entities[i]; - if (entity && oldHass.states[entity] !== newHass.states[entity]) { - return true; - } - } - return false; -} - -/** - * Determine if a MediaShowInfo object is valid/acceptable. - * @param info The MediaShowInfo object. - * @returns True if the object is valid, false otherwise. - */ -export function isValidMediaShowInfo(info: MediaShowInfo): boolean { - return ( - info.height >= MEDIA_INFO_HEIGHT_CUTOFF && info.width >= MEDIA_INFO_WIDTH_CUTOFF - ); -} - -/** - * Convert a generic Action to a FrigateCardCustomAction if it parses correctly. - * @param action The generic action configuration. - * @returns A FrigateCardCustomAction or null if it cannot be converted. - */ -export function convertActionToFrigateCardCustomAction( - action: ActionType | null, -): FrigateCardCustomAction | null { - if (!action) { - return null; - } - // Parse a custom event as other things could generate ll-custom events that - // are not related to Frigate Card. - const parseResult = frigateCardCustomActionSchema.safeParse(action); - return parseResult.success ? parseResult.data : null; -} - -/** - * Create a Frigate card custom action. - * @param action The Frigate card action string (e.g. 'fullscreen') - * @returns A FrigateCardCustomAction for that action string or null. - */ -export function createFrigateCardCustomAction( - action: FrigateCardAction, - args?: { - camera?: string; - media_player?: string; - media_player_action?: 'play' | 'stop'; - }, -): FrigateCardCustomAction | null { - if (action === 'camera_select') { - if (!args?.camera) { - return null; - } - return { - action: 'fire-dom-event', - frigate_card_action: action, - camera: args.camera as string, - }; - } - if (action === 'media_player') { - if (!args?.media_player || !args.media_player_action) { - return null; - } - return { - action: 'fire-dom-event', - frigate_card_action: action, - media_player: args.media_player, - media_player_action: args.media_player_action, - }; - } - return { - action: 'fire-dom-event', - frigate_card_action: action, - }; -} - -/** - * Get an action configuration given a config and an interaction (e.g. 'tap'). - * @param interaction The interaction: `tap`, `hold` or `double_tap` - * @param config The configuration containing multiple actions. - * @returns The relevant action configuration or null if none found. - */ -export function getActionConfigGivenAction( - interaction?: string, - config?: Actions, -): ActionType | ActionType[] | undefined { - if (!interaction || !config) { - return undefined; - } - if (interaction == 'tap' && config.tap_action) { - return config.tap_action; - } else if (interaction == 'hold' && config.hold_action) { - return config.hold_action; - } else if (interaction == 'double_tap' && config.double_tap_action) { - return config.double_tap_action; - } else if (interaction == 'end_tap' && config.end_tap_action) { - return config.end_tap_action; - } else if (interaction == 'start_tap' && config.start_tap_action) { - return config.start_tap_action; - } - return undefined; -} - -/** - * Calculate a style brightness from a hass state. - * Inspired by https://github.com/home-assistant/frontend/blob/7d5b5663123bb16d1da0c5bac3f2fc26d5f69ae8/src/panels/lovelace/cards/hui-button-card.ts#L296 - * @param state The hass state object. - * @returns A CSS brightness string. - */ -function computeBrightnessFromState(state: HassEntity): string { - if (state.state === 'off' || !state.attributes.brightness) { - return ''; - } - const brightness = state.attributes.brightness; - return `brightness(${(brightness + 245) / 5}%)`; -} - -/** - * Calculate a style color from a hass state. - * Inspired by https://github.com/home-assistant/frontend/blob/7d5b5663123bb16d1da0c5bac3f2fc26d5f69ae8/src/panels/lovelace/cards/hui-button-card.ts#L304 - * @param state The hass state object. - * @returns A CSS color string. - */ -function computeColorFromState(state: HassEntity): string { - if (state.state === 'off') { - return ''; - } - return state.attributes.rgb_color - ? `rgb(${state.attributes.rgb_color.join(',')})` - : ''; -} - -/** - * Get the style of emphasized menu items. - * @returns A StyleInfo. - */ -function computeStyle(state: HassEntity): StyleInfo { - return { - color: computeColorFromState(state), - filter: computeBrightnessFromState(state), - }; -} - -/** - * Determine the string state of a given stateObj. - * From: https://github.com/home-assistant/frontend/blob/dev/src/common/entity/compute_active_state.ts - * @param stateObj The HassEntity object from `hass.states`. - * @returns A string state, e.g. 'on'. - */ -export const computeActiveState = (stateObj: HassEntity): string => { - const domain = stateObj.entity_id.split('.')[0]; - let state = stateObj.state; - - if (domain === 'climate') { - state = stateObj.attributes.hvac_action; - } - - return state; -}; - -/** - * Use Home Assistant state to refresh state parameters for an item to be rendered. - * @param hass Home Assistant object. - * @param params A StateParameters object to modify in place. - * @returns A StateParameters object updated based on HASS state. - */ -export function refreshDynamicStateParameters( - hass: HomeAssistant, - params: StateParameters, -): StateParameters { - if (!params.entity) { - return params; - } - const state = hass.states[params.entity]; - if (!!state && !!params.state_color) { - params.style = { ...computeStyle(state), ...params.style }; - } - params.title = params.title ?? (state?.attributes?.friendly_name || params.entity); - params.icon = params.icon ?? stateIcon(state); - - const domain = state ? computeStateDomain(state) : undefined; - params.data_domain = - params.state_color || (domain === 'light' && params.state_color !== false) - ? domain - : undefined; - if (state) { - params.data_state = computeActiveState(state); - } - return params; -} - -/** - * Prettify a Frigate name by converting '_' to spaces and capitalizing words. - * @param input The input Frigate (camera/label/zone) name. - * @returns A prettified name. - */ -export function prettifyFrigateName(input?: string): string | undefined { - if (!input) { - return undefined; - } - const words = input.split(/[_\s]+/); - return words - .map((word) => { - return word[0].toUpperCase() + word.substring(1); - }) - .join(' '); -} - -/** - * Get the title of an entity. - * @param entity The entity id. - * @param hass The Home Assistant object. - * @returns The title or undefined. - */ -export function getEntityTitle( - hass?: HomeAssistant, - entity?: string, -): string | undefined { - return entity ? hass?.states[entity]?.attributes?.friendly_name : undefined; -} - -/** - * Get the icon of an entity. - * @param entity The entity id. - * @param hass The Home Assistant object. - * @returns The icon or undefined. - */ -export function getEntityIcon(hass?: HomeAssistant, entity?: string): string { - return stateIcon(entity ? hass?.states[entity] : null); -} - -/** - * Get a camera id. - * @param config The camera config (either parsed or raw). - * @returns A camera id. - */ -export function getCameraID( - config?: CameraConfig | RawFrigateCardConfig | null, -): string { - return ( - (typeof config?.id === 'string' && config.id) || - (typeof config?.camera_entity === 'string' && config.camera_entity) || - (typeof config?.webrtc_card === 'object' && - config.webrtc_card && - typeof config.webrtc_card['entity'] === 'string' && - config.webrtc_card['entity']) || - (typeof config?.camera_name === 'string' && config.camera_name) || - '' - ); -} - -/** - * Get a camera text title. - * @param hass The Home Assistant object. - * @param config The camera config (either parsed or raw). - * @returns A title string. - */ -export function getCameraTitle( - hass?: HomeAssistant, - config?: CameraConfig | RawFrigateCardConfig | null, -): string { - // Attempt to render a recognizable name for the camera, - // starting with the most likely to be useful and working our - // ways towards the least useful. Extra type checking here since this is also - // used on raw configuration in the editor. - return ( - (typeof config?.title === 'string' && config.title) || - (typeof config?.camera_entity === 'string' - ? getEntityTitle(hass, config.camera_entity) - : '') || - (typeof config?.webrtc_card === 'object' && - config.webrtc_card && - typeof config.webrtc_card['entity'] === 'string' && - config.webrtc_card['entity']) || - (typeof config?.camera_name === 'string' - ? prettifyFrigateName(config.camera_name) - : '') || - (typeof config?.id === 'string' && config.id) || - '' - ); -} - -/** - * Get a camera icon. - * @param hass The Home Assistant object. - * @param config The camera config. - * @returns An icon string. - */ -export function getCameraIcon( - hass?: HomeAssistant, - config?: CameraConfig | null, -): string { - return config?.icon || getEntityIcon(hass, config?.camera_entity) || 'mdi:video'; -} - -/** - * Move an element within an array. - * @param target Target array. - * @param from From index. - * @param to To index. - */ -export function arrayMove(target: unknown[], from: number, to: number): void { - const element = target[from]; - target.splice(from, 1); - target.splice(to, 0, element); -} - -/** - * Determine if the contents of the n(ew) and o(ld) values have changed. For use - * in lit web components that may have a value that changes address but not - * contents -- and for which a re-render is expensive/jarring. - * @param n The new value. - * @param o The old value. - * @returns `true` is the contents have changed. - */ -export function contentsChanged(n: unknown, o: unknown): boolean { - return !isEqual(n, o); -} - -/** - * Frigate card custom version of handleAction - * (https://github.com/custom-cards/custom-card-helpers/blob/master/src/handle-action.ts) - * that handles the custom action events the card supports. - * @param node The node that fired the event. - * @param hass The Home Assistant object. - * @param config The multi-action configuration. - * @param action The action string (e.g. 'hold') - * @returns Whether or not an action was executed. - */ -export const frigateCardHandleAction = ( - node: HTMLElement, - hass: HomeAssistant, - config: ActionsConfig, - action: string, -): boolean => { - return frigateCardHandleActionConfig( - node, - hass, - config, - action, - getActionConfigGivenAction(action, config), - ); -}; - -/** - * Handle an ActionConfig or array of ActionConfigs. - * @param node The node that fired the event. - * @param hass The Home Assistant object. - * @param actionConfig A single action config, array of action configs or - * undefined for the default action config for 'tap'. - * @param action The action string (e.g. 'hold') - * @returns Whether or not an action was executed. - */ -export const frigateCardHandleActionConfig = ( - node: HTMLElement, - hass: HomeAssistant, - config: { - camera_image?: string; - entity?: string; - }, - action: string, - actionConfig: ActionType | ActionType[] | undefined, -): boolean => { - // Only allow a tap action to use a default non-config (the more-info config). - if (actionConfig || action == 'tap') { - // ActionConfig vs ActionType: - // There is a slight typing (but not functional) difference between - // ActionType in this card and ActionConfig in `custom-card-helpers`. See - // `ExtendedConfirmationRestrictionConfig` in `types.ts` for the source and - // reason behind this difference. - if (Array.isArray(actionConfig)) { - actionConfig.forEach((action) => - handleActionConfig(node, hass, config, action as ActionConfig | undefined), - ); - } else { - handleActionConfig(node, hass, config, actionConfig as ActionConfig | undefined); - } - return true; - } - return false; -}; - -/** - * Determine if an action config has a real action. A modified version of - * custom-card-helpers hasAction to also work with arrays of action configs. - * @param config The action config in question. - * @returns `true` if there's a real action defined, `false` otherwise. - */ -export const frigateCardHasAction = ( - config?: ActionType | ActionType[] | undefined, -): boolean => { - // See note above on 'ActionConfig vs ActionType' for why this cast is - // necessary and harmless. - if (Array.isArray(config)) { - return !!config.find((item) => hasAction(item as ActionConfig | undefined)); - } - return hasAction(config as ActionConfig | undefined); -}; - -/** - * Stop an event from activating card wide actions. - */ -export const stopEventFromActivatingCardWideActions = (ev: Event): void => { - ev.stopPropagation(); -}; - -/** - * Convenience function to convert a timestamp to hours, minutes and seconds - * string. Heavily inspired by, and returning the same format as, the Frigate - * UI: https://github.com/blakeblackshear/frigate/blob/master/web/src/components/RecordingPlaylist.jsx#L97 - * @param event The Frigate event. - * @returns A duration string. - */ -export function getEventDurationString(event: FrigateEvent): string { - if (!event.end_time) { - return localize('event.in_progress'); - } - const start = fromUnixTime(event.start_time); - const end = fromUnixTime(event.end_time); - const hours = differenceInHours(end, start); - const minutes = differenceInMinutes(end, start) - hours * 60; - const seconds = differenceInSeconds(end, start) - hours * 60 * 60 - minutes * 60; - let duration = ''; - - if (hours) { - duration += `${hours}h `; - } - if (minutes) { - duration += `${minutes}m `; - } - duration += `${seconds}s`; - return duration; -} - -/** - * Side loads the HA elements this card needs. This trickery is unfortunate - * necessary, see: - * - https://github.com/thomasloven/hass-config/wiki/PreLoading-Lovelace-Elements - * @returns `true` if the load is successful, `false` otherwise. - */ -export const sideLoadHomeAssistantElements = async (): Promise => { - const neededElements = [ - 'ha-selector', - 'ha-menu-button', - 'ha-camera-stream', - 'ha-hls-player', - 'ha-web-rtc-player', - 'ha-icon', - 'ha-circular-progress', - 'ha-icon-button', - 'ha-card', - 'ha-svg-icon', - 'ha-button-menu', - ]; - - if (neededElements.every((element) => customElements.get(element))) { - return true; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const helpers: CardHelpers = await (window as any).loadCardHelpers(); - - // The picture-glance editor loads everything this card needs. - const pictureGlance = await helpers.createCardElement({ - type: 'picture-glance', - entities: [], - camera_image: 'dummy-to-load-editor-components', - }); - if (pictureGlance.constructor.getConfigElement) { - await pictureGlance.constructor.getConfigElement(); - return true; - } - return false; -}; diff --git a/src/components/carousel.ts b/src/components/carousel.ts index 9cd9b923..9da0102f 100644 --- a/src/components/carousel.ts +++ b/src/components/carousel.ts @@ -1,17 +1,14 @@ -import { CSSResultGroup, LitElement, unsafeCSS, PropertyValues } from 'lit'; -import { property } from 'lit/decorators.js'; - import EmblaCarousel, { EmblaCarouselType, EmblaOptionsType, - EmblaPluginType, + EmblaPluginType } from 'embla-carousel'; import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures'; - -import { TransitionEffect } from '../types'; -import { dispatchFrigateCardEvent } from '../common'; - +import { CSSResultGroup, LitElement, PropertyValues, unsafeCSS } from 'lit'; +import { property } from 'lit/decorators.js'; import carouselStyle from '../scss/carousel.scss'; +import { TransitionEffect } from '../types'; +import { dispatchFrigateCardEvent } from '../utils/basic.js'; export interface CarouselSelect { index: number; diff --git a/src/components/elements.ts b/src/components/elements.ts index 2eada158..e815a5fe 100644 --- a/src/components/elements.ts +++ b/src/components/elements.ts @@ -1,28 +1,27 @@ -import { - LitElement, - TemplateResult, - html, - CSSResultGroup, - unsafeCSS, - PropertyValues, -} from 'lit'; import { HomeAssistant } from 'custom-card-helpers'; +import { + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, + unsafeCSS +} from 'lit'; import { customElement, property } from 'lit/decorators.js'; - +import { ConditionState, fetchStateAndEvaluateCondition } from '../card-condition.js'; +import { localize } from '../localize/localize.js'; +import elementsStyle from '../scss/elements.scss'; import { FrigateConditional, MenuButton, MenuIcon, MenuStateIcon, - PictureElements, MenuSubmenu, MenuSubmenuSelect, + PictureElements } from '../types.js'; -import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; - -import elementsStyle from '../scss/elements.scss'; -import { localize } from '../localize/localize.js'; -import { ConditionState, fetchStateAndEvaluateCondition } from '../card-condition.js'; +import { dispatchFrigateCardEvent } from '../utils/basic.js'; +import { dispatchErrorMessageEvent } from './message.js'; /* A note on picture element rendering: * diff --git a/src/components/gallery.ts b/src/components/gallery.ts index 1bdcf2a7..80fabf2a 100644 --- a/src/components/gallery.ts +++ b/src/components/gallery.ts @@ -1,26 +1,27 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { HomeAssistant } from 'custom-card-helpers'; import { + css, CSSResultGroup, + html, LitElement, PropertyValues, TemplateResult, - css, - html, - unsafeCSS, + unsafeCSS } from 'lit'; -import { HomeAssistant } from 'custom-card-helpers'; import { customElement, property } from 'lit/decorators.js'; - -import { CameraConfig, GalleryConfig, frigateCardConfigDefaults } from '../types.js'; -import { BrowseMediaUtil } from '../browse-media-util.js'; -import { View } from '../view.js'; -import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js'; -import { renderProgressIndicator } from './message.js'; -import { stopEventFromActivatingCardWideActions } from '../common.js'; - -import './thumbnail.js'; - import galleryStyle from '../scss/gallery.scss'; +import { CameraConfig, frigateCardConfigDefaults, GalleryConfig } from '../types.js'; +import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; +import { + fetchChildMediaAndDispatchViewChange, + fetchLatestMediaAndDispatchViewChange, + getFullDependentBrowseMediaQueryParametersOrDispatchError +} from '../utils/ha/browse-media'; +import { View } from '../view.js'; +import { renderProgressIndicator } from './message.js'; +import './thumbnail.js'; +import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js'; @customElement('frigate-card-gallery') export class FrigateCardGallery extends LitElement { @@ -54,7 +55,7 @@ export class FrigateCardGallery extends LitElement { if (!this.view.target) { const browseMediaQueryParameters = - BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError( + getFullDependentBrowseMediaQueryParametersOrDispatchError( this, this.hass, this.cameras, @@ -66,7 +67,7 @@ export class FrigateCardGallery extends LitElement { return; } - BrowseMediaUtil.fetchLatestMediaAndDispatchViewChange( + fetchLatestMediaAndDispatchViewChange( this, this.hass, this.view, @@ -219,7 +220,7 @@ export class FrigateCardGalleryCore extends LitElement { { if (this.hass && this.view) { - BrowseMediaUtil.fetchChildMediaAndDispatchViewChange( + fetchChildMediaAndDispatchViewChange( this, this.hass, this.view, diff --git a/src/components/image.ts b/src/components/image.ts index 28d1c0de..0ba8f042 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -1,28 +1,23 @@ +import { HomeAssistant } from 'custom-card-helpers'; import { CSSResultGroup, + html, LitElement, PropertyValues, TemplateResult, - html, - unsafeCSS, + unsafeCSS } from 'lit'; -import { HomeAssistant } from 'custom-card-helpers'; -import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { customElement, property, state } from 'lit/decorators.js'; - +import { createRef, ref, Ref } from 'lit/directives/ref.js'; import { CachedValueController } from '../cached-value-controller.js'; -import { CameraConfig, ImageViewConfig } from '../types.js'; -import { View } from '../view.js'; -import { - dispatchErrorMessageEvent, - dispatchMediaShowEvent, - shouldUpdateBasedOnHass, -} from '../common.js'; -import { localize } from '../localize/localize.js'; - import defaultImage from '../images/frigate-bird-in-sky.jpg'; - +import { localize } from '../localize/localize.js'; import imageStyle from '../scss/image.scss'; +import { CameraConfig, ImageViewConfig } from '../types.js'; +import { shouldUpdateBasedOnHass } from '../utils/ha'; +import { dispatchMediaShowEvent } from '../utils/media-info.js'; +import { View } from '../view.js'; +import { dispatchErrorMessageEvent } from './message.js'; // See: https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py#L101 const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000; @@ -41,7 +36,7 @@ export class FrigateCardImage extends LitElement { @state() protected _imageConfig?: ImageViewConfig; - protected _refImage: Ref = createRef() ; + protected _refImage: Ref = createRef(); protected _cachedValueController?: CachedValueController; protected _boundVisibilityHandler = this._visibilityHandler.bind(this); diff --git a/src/components/live.ts b/src/components/live.ts index 7c1d52ce..6c78aa43 100644 --- a/src/components/live.ts +++ b/src/components/live.ts @@ -1,60 +1,56 @@ +import JSMpeg from '@cycjimmy/jsmpeg-player'; +import { Task } from '@lit-labs/task'; +import { HomeAssistant } from 'custom-card-helpers'; +import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { CSSResultGroup, - LitElement, - TemplateResult, html, - unsafeCSS, + LitElement, PropertyValues, + TemplateResult, + unsafeCSS } from 'lit'; -import { - ExtendedHomeAssistant, - CameraConfig, - JSMPEGConfig, - LiveConfig, - MediaShowInfo, - WebRTCCardConfig, - FrigateCardError, - FrigateCardMediaPlayer, - LiveOverrides, - LiveProvider, - TransitionEffect, - frigateCardConfigDefaults, -} from '../types.js'; -import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; -import { HomeAssistant } from 'custom-card-helpers'; -import JSMpeg from '@cycjimmy/jsmpeg-player'; -import { Ref, createRef, ref } from 'lit/directives/ref.js'; -import { Task } from '@lit-labs/task'; import { customElement, property, state } from 'lit/decorators.js'; +import { createRef, Ref, ref } from 'lit/directives/ref.js'; import { until } from 'lit/directives/until.js'; - -import { AutoMediaPlugin, AutoMediaPluginType } from './embla-plugins/automedia.js'; -import { BrowseMediaUtil } from '../browse-media-util.js'; import { ConditionState, getOverriddenConfig } from '../card-condition.js'; -import { FrigateCardMediaCarousel } from './media-carousel.js'; -import { FrigateCardNextPreviousControl } from './next-prev-control.js'; -import { Lazyload } from './embla-plugins/lazyload.js'; -import { View } from '../view.js'; -import { localize } from '../localize/localize.js'; -import { - contentsChanged, - dispatchErrorMessageEvent, - dispatchExistingMediaShowInfoAsEvent, - dispatchMediaShowEvent, - getCameraIcon, - getCameraTitle, - homeAssistantSignPath, - stopEventFromActivatingCardWideActions, -} from '../common.js'; import { renderProgressIndicator } from '../components/message.js'; - -import './next-prev-control.js'; -import './title-control.js'; - -import liveStyle from '../scss/live.scss'; +import { localize } from '../localize/localize.js'; import liveFrigateStyle from '../scss/live-frigate.scss'; import liveJSMPEGStyle from '../scss/live-jsmpeg.scss'; import liveWebRTCStyle from '../scss/live-webrtc.scss'; +import liveStyle from '../scss/live.scss'; +import { + CameraConfig, + ExtendedHomeAssistant, + frigateCardConfigDefaults, + FrigateCardError, + FrigateCardMediaPlayer, + JSMPEGConfig, + LiveConfig, + LiveOverrides, + LiveProvider, + MediaShowInfo, + TransitionEffect, + WebRTCCardConfig +} from '../types.js'; +import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; +import { contentsChanged } from '../utils/basic.js'; +import { getCameraIcon, getCameraTitle } from '../utils/camera.js'; +import { homeAssistantSignPath } from '../utils/ha'; +import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js'; +import { + dispatchExistingMediaShowInfoAsEvent, + dispatchMediaShowEvent +} from '../utils/media-info.js'; +import { View } from '../view.js'; +import { AutoMediaPlugin, AutoMediaPluginType } from './embla-plugins/automedia.js'; +import { Lazyload } from './embla-plugins/lazyload.js'; +import { FrigateCardMediaCarousel } from './media-carousel.js'; +import { dispatchErrorMessageEvent } from './message.js'; +import './next-prev-control.js'; +import { FrigateCardNextPreviousControl } from './next-prev-control.js'; +import './title-control.js'; // Number of seconds a signed URL is valid for. const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60; @@ -129,7 +125,7 @@ export class FrigateCardLive extends LitElement { // Does not use getFullDependentBrowseMediaQueryParametersOrDispatchError to // ensure that non-Frigate cameras will work in live view (they will not // have a Frigate camera name). - const browseMediaParams = BrowseMediaUtil.getFullDependentBrowseMediaQueryParameters( + const browseMediaParams = getFullDependentBrowseMediaQueryParameters( this.hass, this.cameras, this.view.camera, diff --git a/src/components/media-carousel.ts b/src/components/media-carousel.ts index fa6afa4f..2ff28371 100644 --- a/src/components/media-carousel.ts +++ b/src/components/media-carousel.ts @@ -1,21 +1,18 @@ -import { CSSResultGroup, unsafeCSS } from 'lit'; import { EmblaCarouselType } from 'embla-carousel'; -import { createRef, Ref } from 'lit/directives/ref.js'; +import { CSSResultGroup, unsafeCSS } from 'lit'; import { customElement } from 'lit/decorators.js'; - -import { AutoMediaPluginType } from './embla-plugins/automedia.js'; -import { FrigateCardCarousel } from './carousel.js'; -import { FrigateCardNextPreviousControl } from './next-prev-control.js'; -import { FrigateCardTitleControl } from './title-control.js'; +import { createRef, Ref } from 'lit/directives/ref.js'; +import mediaCarouselStyle from '../scss/media-carousel.scss'; import type { MediaShowInfo } from '../types.js'; import { dispatchExistingMediaShowInfoAsEvent, - isValidMediaShowInfo, -} from '../common.js'; - + isValidMediaShowInfo +} from '../utils/media-info.js'; +import { FrigateCardCarousel } from './carousel.js'; +import { AutoMediaPluginType } from './embla-plugins/automedia.js'; import './next-prev-control.js'; - -import mediaCarouselStyle from '../scss/media-carousel.scss'; +import { FrigateCardNextPreviousControl } from './next-prev-control.js'; +import { FrigateCardTitleControl } from './title-control.js'; const getEmptyImageSrc = (width: number, height: number) => `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`; diff --git a/src/components/menu.ts b/src/components/menu.ts index 07888542..a58b07e3 100644 --- a/src/components/menu.ts +++ b/src/components/menu.ts @@ -1,38 +1,34 @@ +import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers'; import { CSSResultGroup, - LitElement, - TemplateResult, html, - unsafeCSS, + LitElement, PropertyValues, + TemplateResult, + unsafeCSS } from 'lit'; -import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers'; import { customElement, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { styleMap } from 'lit/directives/style-map.js'; - import { actionHandler } from '../action-handler-directive.js'; - -import './submenu.js'; - +import menuStyle from '../scss/menu.scss'; import type { ActionsConfig, ActionType, MenuButton, MenuConfig, MenuItem, - StateParameters, + StateParameters } from '../types.js'; import { convertActionToFrigateCardCustomAction, frigateCardHandleActionConfig, frigateCardHasAction, - getActionConfigGivenAction, - refreshDynamicStateParameters, -} from '../common.js'; - -import menuStyle from '../scss/menu.scss'; + getActionConfigGivenAction +} from '../utils/action.js'; +import { refreshDynamicStateParameters } from '../utils/ha'; +import './submenu.js'; export const FRIGATE_BUTTON_MENU_ICON = 'frigate'; export const FRIGATE_ICON_FILLED = diff --git a/src/components/message.ts b/src/components/message.ts index cc947653..12547788 100644 --- a/src/components/message.ts +++ b/src/components/message.ts @@ -1,11 +1,10 @@ -import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; - -import { Message } from '../types.js'; import { TROUBLESHOOTING_URL } from '../const.js'; import { localize } from '../localize/localize.js'; - import messageStyle from '../scss/message.scss'; +import { Message } from '../types.js'; +import { dispatchFrigateCardEvent } from '../utils/basic.js'; @customElement('frigate-card-message') export class FrigateCardMessage extends LitElement { @@ -21,18 +20,19 @@ export class FrigateCardMessage extends LitElement { // Render the menu. protected render(): TemplateResult { const icon = this.icon ? this.icon : 'mdi:information-outline'; - return html` -
-
-
- -
-
- ${this.message ? html`${this.message}` : ''} - ${this.context ? html`
${JSON.stringify(this.context, null, 2)}
` : ''} -
+ return html`
+
+
+
-
`; +
+ ${this.message ? html`${this.message}` : ''} + ${this.context + ? html`
${JSON.stringify(this.context, null, 2)}
` + : ''} +
+
+
`; } static get styles(): CSSResultGroup { @@ -103,3 +103,40 @@ export function renderProgressIndicator(message?: string): TemplateResult { `; } + +/** + * Dispatch an event with a message to show to the user. + * @param element The element to send the event. + * @param message The message to show. + * @param icon An optional icon to attach to the message. + */ +export function dispatchMessageEvent( + element: HTMLElement, + message: string, + icon?: string, + context?: unknown, +): void { + dispatchFrigateCardEvent(element, 'message', { + message: message, + type: 'info', + icon: icon, + context: context, + }); +} + +/** + * Dispatch an event with an error message to show to the user. + * @param element The element to send the event. + * @param message The message to show. + */ +export function dispatchErrorMessageEvent( + element: HTMLElement, + message: string, + context?: unknown, +): void { + dispatchFrigateCardEvent(element, 'message', { + message: message, + type: 'error', + context: context, + }); +} diff --git a/src/components/submenu.ts b/src/components/submenu.ts index df601177..f7a77562 100644 --- a/src/components/submenu.ts +++ b/src/components/submenu.ts @@ -1,29 +1,25 @@ import type { Corner } from '@material/mwc-menu'; +import { HomeAssistant } from 'custom-card-helpers'; import { CSSResultGroup, html, LitElement, PropertyValues, TemplateResult, - unsafeCSS, + unsafeCSS } from 'lit'; -import { HomeAssistant } from 'custom-card-helpers'; import { customElement, property } from 'lit/decorators.js'; - -import { - frigateCardHasAction, - refreshDynamicStateParameters, - shouldUpdateBasedOnHass, - stopEventFromActivatingCardWideActions, -} from '../common.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { styleMap } from 'lit/directives/style-map.js'; - -import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../types.js'; import { actionHandler } from '../action-handler-directive.js'; -import { domainIcon } from '../icons/domain-icon.js'; - import submenuStyle from '../scss/submenu.scss'; +import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../types.js'; +import { + frigateCardHasAction, + stopEventFromActivatingCardWideActions +} from '../utils/action.js'; +import { refreshDynamicStateParameters, shouldUpdateBasedOnHass } from '../utils/ha'; +import { domainIcon } from '../utils/icons/domain-icon.js'; @customElement('frigate-card-submenu') export class FrigateCardSubmenu extends LitElement { diff --git a/src/components/surround-thumbnails.ts b/src/components/surround-thumbnails.ts index 9bd707f9..b65267b7 100644 --- a/src/components/surround-thumbnails.ts +++ b/src/components/surround-thumbnails.ts @@ -1,23 +1,23 @@ -import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; -import { HomeAssistant } from 'custom-card-helpers'; import { Task } from '@lit-labs/task'; +import { HomeAssistant } from 'custom-card-helpers'; +import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property } from 'lit/decorators.js'; - -import { BrowseMediaUtil } from '../browse-media-util.js'; +import surroundThumbnailsStyle from '../scss/surround.scss'; import { BrowseMediaQueryParameters, FrigateBrowseMediaSource, FrigateCardView, - ThumbnailsControlConfig, + ThumbnailsControlConfig } from '../types.js'; - -import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; +import { dispatchFrigateCardEvent } from '../utils/basic.js'; +import { + getFirstTrueMediaChildIndex, + multipleBrowseMediaQueryMerged +} from '../utils/ha/browse-media'; import { View } from '../view.js'; -import { dispatchErrorMessageEvent, dispatchFrigateCardEvent } from '../common.js'; - +import { dispatchErrorMessageEvent } from './message.js'; import './surround.js'; - -import surroundThumbnailsStyle from '../scss/surround.scss'; +import { ThumbnailCarouselTap } from './thumbnail-carousel.js'; @customElement('frigate-card-surround-thumbnails') export class FrigateCardSurround extends LitElement { @@ -71,11 +71,11 @@ export class FrigateCardSurround extends LitElement { } let parent: FrigateBrowseMediaSource | null; try { - parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(this.hass, browseMediaParams); + parent = await multipleBrowseMediaQueryMerged(this.hass, browseMediaParams); } catch (e) { return dispatchErrorMessageEvent(this, (e as Error).message); } - if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) !== null) { + if (getFirstTrueMediaChildIndex(parent) !== null) { this.view ?.evolve({ ...(this.targetView && { view: this.targetView }), diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts index bfa0b36b..d62d1711 100644 --- a/src/components/thumbnail-carousel.ts +++ b/src/components/thumbnail-carousel.ts @@ -1,22 +1,16 @@ -import { BrowseMediaUtil } from '../browse-media-util.js'; -import { CSSResultGroup, TemplateResult, html, unsafeCSS, PropertyValues } from 'lit'; import { EmblaOptionsType } from 'embla-carousel'; -import { classMap } from 'lit/directives/class-map.js'; +import { CSSResultGroup, html, PropertyValues, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; +import { classMap } from 'lit/directives/class-map.js'; import { ifDefined } from 'lit/directives/if-defined.js'; - -import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; -import { FrigateCardCarousel } from './carousel.js'; -import { View } from '../view.js'; -import { - contentsChanged, - dispatchFrigateCardEvent, - stopEventFromActivatingCardWideActions, -} from '../common.js'; - -import './thumbnail.js'; - import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss'; +import type { FrigateBrowseMediaSource, ThumbnailsControlConfig } from '../types.js'; +import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; +import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js'; +import { isTrueMedia } from '../utils/ha/browse-media'; +import { View } from '../view.js'; +import { FrigateCardCarousel } from './carousel.js'; +import './thumbnail.js'; export interface ThumbnailCarouselTap { slideIndex: number; @@ -160,7 +154,7 @@ export class FrigateCardThumbnailCarousel extends FrigateCardCarousel { if ( !parent.children || !parent.children.length || - !BrowseMediaUtil.isTrueMedia(parent.children[childIndex]) + !isTrueMedia(parent.children[childIndex]) ) { return; } diff --git a/src/components/thumbnail.ts b/src/components/thumbnail.ts index 4a4480a4..d5315c9c 100644 --- a/src/components/thumbnail.ts +++ b/src/components/thumbnail.ts @@ -1,18 +1,14 @@ -import { CSSResult, TemplateResult, html, unsafeCSS, LitElement } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; import { format, fromUnixTime } from 'date-fns'; - -import type { FrigateBrowseMediaSource, FrigateEvent } from '../types.js'; -import { View } from '../view.js'; -import { - getEventDurationString, - prettifyFrigateName, - stopEventFromActivatingCardWideActions, -} from '../common.js'; +import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; import { localize } from '../localize/localize.js'; - -import thumbnailStyle from '../scss/thumbnail.scss'; import thumbnailDetailsStyle from '../scss/thumbnail-details.scss'; +import thumbnailStyle from '../scss/thumbnail.scss'; +import type { FrigateBrowseMediaSource, FrigateEvent } from '../types.js'; +import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; +import { prettifyTitle } from '../utils/basic.js'; +import { getEventDurationString } from '../utils/ha/browse-media.js'; +import { View } from '../view.js'; // The minimum width of a thumbnail with details enabled. export const THUMBNAIL_DETAILS_WIDTH_MIN = 300; @@ -28,14 +24,14 @@ export class FrigateCardThumbnailDetails extends LitElement { } const score = (this.event.top_score * 100).toFixed(2) + '%'; return html`
-
${prettifyFrigateName(this.event.label)}
+
${prettifyTitle(this.event.label)}
- ${localize('event.start')}: - ${format(fromUnixTime(this.event.start_time), 'HH:mm:ss')} + ${localize('event.start')}: + ${format(fromUnixTime(this.event.start_time), 'HH:mm:ss')}
- ${localize('event.duration')}: - ${getEventDurationString(this.event)} + ${localize('event.duration')}: + ${getEventDurationString(this.event)}
diff --git a/src/components/timeline.ts b/src/components/timeline.ts index 108aa6f7..3b8ba9bf 100644 --- a/src/components/timeline.ts +++ b/src/components/timeline.ts @@ -1,11 +1,17 @@ +import { HomeAssistant } from 'custom-card-helpers'; +import { add, fromUnixTime, sub } from 'date-fns'; import { CSSResultGroup, - LitElement, - TemplateResult, html, - unsafeCSS, + LitElement, PropertyValues, + 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 { isEqual } from 'lodash-es'; import { DataSet } from 'vis-data/esnext'; import { DataGroupCollectionType, @@ -14,38 +20,31 @@ import { TimelineItem, TimelineOptions, TimelineOptionsCluster, - TimelineWindow, + TimelineWindow } from 'vis-timeline/esnext'; -import { HomeAssistant } from 'custom-card-helpers'; -import { classMap } from 'lit/directives/class-map.js'; -import { customElement, property } from 'lit/decorators.js'; -import { createRef, ref, Ref } from 'lit/directives/ref.js'; -import { add, fromUnixTime, sub } from 'date-fns'; -import { isEqual } from 'lodash-es'; - -import { BrowseMediaUtil } from '../browse-media-util'; +import { CAMERA_BIRDSEYE } from '../const'; +import { localize } from '../localize/localize'; +import timelineCoreStyle from '../scss/timeline-core.scss'; +import timelineStyle from '../scss/timeline.scss'; import { BrowseMediaQueryParameters, CameraConfig, FrigateBrowseMediaSource, - TimelineConfig, - FrigateEvent, frigateCardConfigDefaults, + FrigateEvent, + TimelineConfig } from '../types'; -import { CAMERA_BIRDSEYE } from '../const'; -import { View, ViewContext } from '../view'; +import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; +import { dispatchFrigateCardEvent } from '../utils/basic.js'; +import { getCameraTitle } from '../utils/camera.js'; import { - dispatchErrorMessageEvent, - dispatchFrigateCardEvent, - dispatchMessageEvent, - getCameraTitle, - stopEventFromActivatingCardWideActions, -} from '../common.js'; -import { localize } from '../localize/localize'; - -import timelineCoreStyle from '../scss/timeline-core.scss'; -import timelineStyle from '../scss/timeline.scss'; - + createEventParentForChildren, + getBrowseMediaQueryParameters, + isTrueMedia, + multipleBrowseMediaQuery +} from '../utils/ha/browse-media'; +import { View, ViewContext } from '../view'; +import { dispatchErrorMessageEvent, dispatchMessageEvent } from './message.js'; import './surround-thumbnails.js'; const TIMELINE_EVENT_MANAGER_MAX_AGE_SECONDS = 10; @@ -143,7 +142,7 @@ class TimelineEventManager { const event = child.frigate?.event; if ( event && - BrowseMediaUtil.isTrueMedia(child) && + isTrueMedia(child) && ['video', 'image'].includes(child.media_content_type) ) { let item = this._dataset.get(event.id); @@ -288,20 +287,15 @@ class TimelineEventManager { this._dateStart && cameraConfig.camera_name !== CAMERA_BIRDSEYE ) { - const param = BrowseMediaUtil.getBrowseMediaQueryParameters( - hass, - cameraID, - cameraConfig, - { - // Events are always fetched for the maximum extent of the managed - // range. This is because events may change at any point in time - // (e.g. a long-running event that ends). - before: this._dateEnd.getTime() / 1000, - after: this._dateStart.getTime() / 1000, - unlimited: true, - mediaType: mediaType as 'clips' | 'snapshots', - }, - ); + const param = getBrowseMediaQueryParameters(hass, cameraID, cameraConfig, { + // Events are always fetched for the maximum extent of the managed + // range. This is because events may change at any point in time + // (e.g. a long-running event that ends). + before: this._dateEnd.getTime() / 1000, + after: this._dateStart.getTime() / 1000, + unlimited: true, + mediaType: mediaType as 'clips' | 'snapshots', + }); if (param) { params.push(param); } @@ -315,7 +309,7 @@ class TimelineEventManager { let results: Map; try { - results = await BrowseMediaUtil.multipleBrowseMediaQuery(hass, params); + results = await multipleBrowseMediaQuery(hass, params); } catch (e) { return dispatchErrorMessageEvent(element, (e as Error).message); } @@ -578,10 +572,7 @@ export class FrigateCardTimelineCore extends LitElement { return null; } - const target = BrowseMediaUtil.createEventParentForChildren( - 'Timeline events', - children, - ); + const target = createEventParentForChildren('Timeline events', children); return { target: target, childIndex: childIndex < 0 ? null : childIndex, diff --git a/src/components/viewer.ts b/src/components/viewer.ts index c9923252..73c3fd26 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -1,19 +1,21 @@ +import { Task } from '@lit-labs/task'; +import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; import { CSSResultGroup, + html, LitElement, PropertyValues, TemplateResult, - html, - unsafeCSS, + unsafeCSS } from 'lit'; -import { BrowseMediaUtil } from '../browse-media-util.js'; -import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel'; -import { Task } from '@lit-labs/task'; import { customElement, property } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { ref } from 'lit/directives/ref.js'; - -import { AutoMediaPlugin } from './embla-plugins/automedia.js'; +import { + dispatchErrorMessageEvent, + renderProgressIndicator +} from '../components/message.js'; +import viewerStyle from '../scss/viewer.scss'; import type { BrowseMediaNeighbors, BrowseMediaQueryParameters, @@ -22,26 +24,28 @@ import type { FrigateBrowseMediaSource, MediaShowInfo, TransitionEffect, - ViewerConfig, + ViewerConfig } from '../types.js'; -import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js'; -import { FrigateCardNextPreviousControl } from './next-prev-control.js'; -import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js'; -import { ResolvedMediaCache, ResolvedMediaUtil } from '../resolved-media.js'; -import { View } from '../view.js'; +import { stopEventFromActivatingCardWideActions } from '../utils/action.js'; +import { contentsChanged } from '../utils/basic.js'; import { - contentsChanged, - createMediaShowInfo, - dispatchErrorMessageEvent, - stopEventFromActivatingCardWideActions, -} from '../common.js'; -import { renderProgressIndicator } from '../components/message.js'; - + fetchLatestMediaAndDispatchViewChange, + getEventStartTime, + getFullDependentBrowseMediaQueryParametersOrDispatchError, + isTrueMedia, + multipleBrowseMediaQueryMerged, + overrideMultiBrowseMediaQueryParameters +} from '../utils/ha/browse-media.js'; +import { createMediaShowInfo } from '../utils/media-info.js'; +import { ResolvedMediaCache, resolveMedia } from '../utils/resolved-media.js'; +import { View } from '../view.js'; +import { AutoMediaPlugin } from './embla-plugins/automedia.js'; +import { Lazyload, LazyloadType } from './embla-plugins/lazyload.js'; +import { FrigateCardMediaCarousel, IMG_EMPTY } from './media-carousel.js'; import './next-prev-control.js'; +import { FrigateCardNextPreviousControl } from './next-prev-control.js'; import './title-control.js'; -import viewerStyle from '../scss/viewer.scss'; - @customElement('frigate-card-viewer') export class FrigateCardViewer extends LitElement { @property({ attribute: false }) @@ -69,7 +73,7 @@ export class FrigateCardViewer extends LitElement { } const browseMediaQueryParameters = - BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError( + getFullDependentBrowseMediaQueryParametersOrDispatchError( this, this.hass, this.cameras, @@ -86,14 +90,13 @@ export class FrigateCardViewer extends LitElement { return; } - BrowseMediaUtil.fetchLatestMediaAndDispatchViewChange( + fetchLatestMediaAndDispatchViewChange( this, this.hass, this.view, - BrowseMediaUtil.overrideMultiBrowseMediaQueryParameters( - browseMediaQueryParameters, - { mediaType: mediaType }, - ), + overrideMultiBrowseMediaQueryParameters(browseMediaQueryParameters, { + mediaType: mediaType, + }), ); return renderProgressIndicator(); } @@ -164,8 +167,8 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { i < (target.children || []).length; ++i ) { - if (BrowseMediaUtil.isTrueMedia(target.children[i])) { - await ResolvedMediaUtil.resolveMedia( + if (isTrueMedia(target.children[i])) { + await resolveMedia( this.hass, target.children[i], this.resolvedMediaCache, @@ -324,7 +327,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let prevIndex: number | null = null; for (let i = this.view.childIndex - 1; i >= 0; i--) { const media = this.view.target.children[i]; - if (media && BrowseMediaUtil.isTrueMedia(media)) { + if (media && isTrueMedia(media)) { prevIndex = i; break; } @@ -334,7 +337,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let nextIndex: number | null = null; for (let i = this.view.childIndex + 1; i < this.view.target.children.length; i++) { const media = this.view.target.children[i]; - if (media && BrowseMediaUtil.isTrueMedia(media)) { + if (media && isTrueMedia(media)) { nextIndex = i; break; } @@ -368,7 +371,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { return null; } - const snapshotStartTime = BrowseMediaUtil.getEventStartTime(snapshot); + const snapshotStartTime = getEventStartTime(snapshot); if (!snapshotStartTime) { return null; } @@ -386,10 +389,10 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let latest: number | null = null; for (let i = 0; i < this.view.target.children.length; i++) { const child = this.view.target.children[i]; - if (!BrowseMediaUtil.isTrueMedia(child)) { + if (!isTrueMedia(child)) { continue; } - const startTime = BrowseMediaUtil.getEventStartTime(child); + const startTime = getEventStartTime(child); if (startTime && (earliest === null || startTime < earliest)) { earliest = startTime; @@ -404,7 +407,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { let clips: FrigateBrowseMediaSource | null; - const params = BrowseMediaUtil.overrideMultiBrowseMediaQueryParameters( + const params = overrideMultiBrowseMediaQueryParameters( this.browseMediaQueryParameters, { mediaType: 'clips', @@ -414,7 +417,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { ); try { - clips = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(this.hass, params); + clips = await multipleBrowseMediaQueryMerged(this.hass, params); } catch (e) { // This is best effort. return null; @@ -426,10 +429,10 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { for (let i = 0; i < clips.children.length; i++) { const child = clips.children[i]; - if (!BrowseMediaUtil.isTrueMedia(child)) { + if (!isTrueMedia(child)) { continue; } - const clipStartTime = BrowseMediaUtil.getEventStartTime(child); + const clipStartTime = getEventStartTime(child); if (clipStartTime && clipStartTime === snapshotStartTime) { return this.view.evolve({ view: 'clip', @@ -490,12 +493,12 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { !this.view || !this.view.target || !this.view.target.children || - !BrowseMediaUtil.isTrueMedia(this.view.target.children[childIndex]) + !isTrueMedia(this.view.target.children[childIndex]) ) { return; } - ResolvedMediaUtil.resolveMedia( + resolveMedia( this.hass, this.view.target.children[childIndex], this.resolvedMediaCache, @@ -667,7 +670,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel { !this.hass || !this.view || !this.viewerConfig || - !BrowseMediaUtil.isTrueMedia(mediaToRender) || + !isTrueMedia(mediaToRender) || !['video', 'image'].includes(mediaToRender.media_content_type) ) { return; diff --git a/src/editor.ts b/src/editor.ts index b003f60b..981ef41c 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -1,18 +1,15 @@ -import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit'; +import { fireEvent, HomeAssistant, LovelaceCardEditor } from 'custom-card-helpers'; +import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; - -import { HomeAssistant, LovelaceCardEditor, fireEvent } from 'custom-card-helpers'; -import { localize } from './localize/localize.js'; import { - BUTTON_SIZE_MIN, - FRIGATE_MENU_PRIORITY_MAX, - RawFrigateCardConfig, - RawFrigateCardConfigArray, - THUMBNAIL_WIDTH_MAX, - THUMBNAIL_WIDTH_MIN, - frigateCardConfigDefaults, -} from './types.js'; - + copyConfig, + deleteConfigValue, + getArrayConfigPath, + getConfigValue, + isConfigUpgradeable, + setConfigValue, + upgradeConfig, +} from './config-mgmt.js'; import { CONF_CAMERAS, CONF_CAMERAS_ARRAY_CAMERA_ENTITY, @@ -89,23 +86,20 @@ import { CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, } from './const.js'; -import { - arrayMove, - getCameraID, - getCameraTitle, - sideLoadHomeAssistantElements, -} from './common.js'; -import { - copyConfig, - deleteConfigValue, - getArrayConfigPath, - getConfigValue, - isConfigUpgradeable, - setConfigValue, - upgradeConfig, -} from './config-mgmt.js'; - +import { localize } from './localize/localize.js'; import frigate_card_editor_style from './scss/editor.scss'; +import { + BUTTON_SIZE_MIN, + frigateCardConfigDefaults, + FRIGATE_MENU_PRIORITY_MAX, + RawFrigateCardConfig, + RawFrigateCardConfigArray, + THUMBNAIL_WIDTH_MAX, + THUMBNAIL_WIDTH_MIN, +} from './types.js'; +import { arrayMove } from './utils/basic.js'; +import { getCameraID, getCameraTitle } from './utils/camera.js'; +import { sideLoadHomeAssistantElements } from './utils/ha'; const MENU_BUTTONS = 'buttons'; const MENU_CAMERAS = 'cameras'; @@ -1006,12 +1000,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor })}