Merge pull request #800 from dermotduffy/leterbox

Add ability to change media layout
This commit is contained in:
Dermot Duffy
2022-08-11 21:45:16 -07:00
committed by GitHub
22 changed files with 575 additions and 108 deletions
+4 -2
View File
@@ -17,7 +17,8 @@ import { localize } from '../localize/localize.js';
import imageStyle from '../scss/image.scss';
import { CameraConfig, ImageViewConfig } from '../types.js';
import { isHassDifferent } from '../utils/ha';
import { dispatchMediaShowEvent } from '../utils/media-info.js';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
import { View } from '../view.js';
import { dispatchErrorMessageEvent } from './message.js';
@@ -99,6 +100,7 @@ export class FrigateCardImage extends LitElement {
this._getImageSource.bind(this),
);
}
updateElementStyleFromMediaLayoutConfig(this, this.imageConfig?.layout);
}
// If the camera or view changed, immediately discard the old value (view to
@@ -228,7 +230,7 @@ export class FrigateCardImage extends LitElement {
${ref(this._refImage)}
src=${live(src)}
@load=${(ev: Event) => {
dispatchMediaShowEvent(this, ev);
dispatchMediaLoadedEvent(this, ev);
}}
@error=${() => {
if (this.imageConfig?.mode === 'camera') {
+7 -3
View File
@@ -47,7 +47,7 @@ import { homeAssistantSignPath } from '../utils/ha';
import { getFullDependentBrowseMediaQueryParameters } from '../utils/ha/browse-media.js';
import {
dispatchExistingMediaLoadedInfoAsEvent,
dispatchMediaShowEvent,
dispatchMediaLoadedEvent,
dispatchMediaUnloadedEvent,
} from '../utils/media-info.js';
import { dispatchViewContextChangeEvent, View } from '../view.js';
@@ -67,6 +67,7 @@ import { EmblaCarouselPlugins } from './carousel.js';
import { renderTask } from '../utils/task.js';
import { classMap } from 'lit/directives/class-map.js';
import './image';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
// Number of seconds a signed URL is valid for.
const URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
@@ -751,6 +752,9 @@ export class FrigateCardLiveProvider extends LitElement {
dispatchMediaUnloadedEvent(this);
}
}
if (changedProps.has('liveConfig')) {
updateElementStyleFromMediaLayoutConfig(this, this.liveConfig?.layout);
}
}
/**
@@ -1080,7 +1084,7 @@ export class FrigateCardLiveWebRTCCard extends LitElement {
if (onloadeddata) {
onloadeddata.call(video, e);
}
dispatchMediaShowEvent(this, video);
dispatchMediaLoadedEvent(this, video);
};
}
});
@@ -1219,7 +1223,7 @@ export class FrigateCardLiveJSMPEG extends LitElement {
// ignore any subsequent calls.
if (!videoDecoded && this._jsmpegCanvasElement) {
videoDecoded = true;
dispatchMediaShowEvent(this, this._jsmpegCanvasElement);
dispatchMediaLoadedEvent(this, this._jsmpegCanvasElement);
resolve(player);
}
},
+17
View File
@@ -54,6 +54,7 @@ import '../patches/ha-hls-player';
import './surround-thumbnails';
import { EmblaCarouselPlugins } from './carousel.js';
import { renderTask } from '../utils/task.js';
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
@customElement('frigate-card-viewer')
export class FrigateCardViewer extends LitElement {
@@ -573,6 +574,16 @@ export class FrigateCardViewerCarousel extends LitElement {
return true;
}
/**
* Called when an update will occur.
* @param changedProps The changed properties
*/
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('viewerConfig')) {
updateElementStyleFromMediaLayoutConfig(this, this.viewerConfig?.layout);
}
}
/**
* Render the element, resolving the media first if necessary.
*/
@@ -664,6 +675,12 @@ export class FrigateCardViewerCarousel extends LitElement {
}
}
/**
* Render a single media item in the viewer carousel.
* @param mediaToRender The FrigateBrowseMediaSource to render.
* @param slideIndex The index of the slide to render.
* @returns A rendered template.
*/
protected _renderMediaItem(
mediaToRender: FrigateBrowseMediaSource,
slideIndex: number,
+13 -1
View File
@@ -88,6 +88,11 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE =
`${CONF_MEDIA_VIEWER}.controls.title.mode` as const;
export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_MEDIA_VIEWER}.controls.title.duration_seconds` as const;
export const CONF_MEDIA_VIEWER_LAYOUT_FIT = `${CONF_MEDIA_VIEWER}.layout.fit` as const;
export const CONF_MEDIA_VIEWER_LAYOUT_POSITION_X =
`${CONF_MEDIA_VIEWER}.layout.position.x` as const;
export const CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y =
`${CONF_MEDIA_VIEWER}.layout.position.y` as const;
export const CONF_LIVE = 'live' as const;
export const CONF_LIVE_AUTO_PLAY = `${CONF_LIVE}.auto_play` as const;
@@ -113,16 +118,23 @@ export const CONF_LIVE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL =
export const CONF_LIVE_CONTROLS_TITLE_MODE = `${CONF_LIVE}.controls.title.mode` as const;
export const CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS =
`${CONF_LIVE}.controls.title.duration_seconds` as const;
export const CONF_LIVE_LAYOUT_FIT = `${CONF_LIVE}.layout.fit` as const;
export const CONF_LIVE_LAYOUT_POSITION_X = `${CONF_LIVE}.layout.position.x` as const;
export const CONF_LIVE_LAYOUT_POSITION_Y = `${CONF_LIVE}.layout.position.y` as const;
export const CONF_LIVE_DRAGGABLE = `${CONF_LIVE}.draggable` as const;
export const CONF_LIVE_JSMPEG = `${CONF_LIVE}.jsmpeg` as const;
export const CONF_LIVE_LAZY_LOAD = `${CONF_LIVE}.lazy_load` as const;
export const CONF_LIVE_LAZY_UNLOAD = `${CONF_LIVE}.lazy_unload` as const;
export const CONF_LIVE_PRELOAD = `${CONF_LIVE}.preload` as const;
export const CONF_LIVE_TRANSITION_EFFECT = `${CONF_LIVE}.transition_effect` as const;
export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD = `${CONF_LIVE}.show_image_during_load` as const;
export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD =
`${CONF_LIVE}.show_image_during_load` as const;
export const CONF_LIVE_WEBRTC_CARD = `${CONF_LIVE}.webrtc_card` as const;
export const CONF_IMAGE = 'image' as const;
export const CONF_IMAGE_LAYOUT_FIT = `${CONF_IMAGE}.layout.fit` as const;
export const CONF_IMAGE_LAYOUT_POSITION_X = `${CONF_IMAGE}.layout.position.x` as const;
export const CONF_IMAGE_LAYOUT_POSITION_Y = `${CONF_IMAGE}.layout.position.y` as const;
export const CONF_IMAGE_MODE = `${CONF_IMAGE}.mode` as const;
export const CONF_IMAGE_REFRESH_SECONDS = `${CONF_IMAGE}.refresh_seconds` as const;
export const CONF_IMAGE_URL = `${CONF_IMAGE}.url` as const;
+117 -38
View File
@@ -36,6 +36,9 @@ import {
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
CONF_EVENT_GALLERY_CONTROLS_THUMBNAILS_SIZE,
CONF_IMAGE_LAYOUT_FIT,
CONF_IMAGE_LAYOUT_POSITION_X,
CONF_IMAGE_LAYOUT_POSITION_Y,
CONF_IMAGE_MODE,
CONF_IMAGE_REFRESH_SECONDS,
CONF_IMAGE_URL,
@@ -54,6 +57,9 @@ import {
CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS,
CONF_LIVE_CONTROLS_TITLE_MODE,
CONF_LIVE_DRAGGABLE,
CONF_LIVE_LAYOUT_FIT,
CONF_LIVE_LAYOUT_POSITION_X,
CONF_LIVE_LAYOUT_POSITION_Y,
CONF_LIVE_LAZY_LOAD,
CONF_LIVE_LAZY_UNLOAD,
CONF_LIVE_PRELOAD,
@@ -73,6 +79,9 @@ import {
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
CONF_MEDIA_VIEWER_DRAGGABLE,
CONF_MEDIA_VIEWER_LAYOUT_FIT,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_X,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y,
CONF_MEDIA_VIEWER_LAZY_LOAD,
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
CONF_MENU_ALIGNMENT,
@@ -125,8 +134,11 @@ const MENU_CAMERAS_FRIGATE = 'cameras.frigate';
const MENU_CAMERAS_TRIGGERS = 'cameras.triggers';
const MENU_CAMERAS_WEBRTC = 'cameras.webrtc';
const MENU_EVENT_GALLERY_CONTROLS = 'event_gallery.controls';
const MENU_IMAGE_LAYOUT = 'image.layout';
const MENU_LIVE_CONTROLS = 'live.controls';
const MENU_LIVE_LAYOUT = 'live.layout';
const MENU_MEDIA_VIEWER_CONTROLS = 'media_viewer.controls';
const MENU_MEDIA_VIEWER_LAYOUT = 'media_viewer.layout';
const MENU_TIMELINE_CONTROLS = 'timeline.controls';
const MENU_OPTIONS = 'options';
const MENU_VIEW_SCAN = 'scan';
@@ -402,6 +414,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
{ value: 'never', label: localize('config.common.media_action_conditions.never') },
];
protected _layoutFits: EditorSelectOption[] = [
{ value: '', label: '' },
{ value: 'contain', label: localize('config.common.layout.fits.contain') },
{ value: 'cover', label: localize('config.common.layout.fits.cover') },
{ value: 'fill', label: localize('config.common.layout.fits.fill') },
];
public setConfig(config: RawFrigateCardConfig): void {
// Note: This does not use Zod to parse the configuration, so it may be
// partially or completely invalid. It's more useful to have a partially
@@ -763,6 +782,43 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
</div>`;
}
/**
* Render a media layout section.
* @param domain The submenu domain.
* @param labelPath The path to the label.
* @param configPathFit The path to the fit config.
* @param configPathPositionX The path to the position.x config.
* @param configPathPositionY The path to the position.y config.
* @returns A rendered template.
*/
protected _renderMediaLayout(
domain: string,
labelPath: string,
configPathFit: string,
configPathPositionX: string,
configPathPositionY: string,
): TemplateResult | void {
return this._putInSubmenu(
domain,
true,
labelPath,
{ name: 'mdi:page-layout-body' },
html`
${this._renderOptionSelector(configPathFit, this._layoutFits)}
${this._renderNumberInput(configPathPositionX, {
min: 0,
max: 100,
label: localize('config.common.layout.position.x'),
})}
${this._renderNumberInput(configPathPositionY, {
min: 0,
max: 100,
label: localize('config.common.layout.position.y'),
})}
`,
);
}
/**
* Render a camera section.
* @param cameras The full array of cameras.
@@ -1258,6 +1314,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
)}
`,
)}
${this._renderMediaLayout(
MENU_LIVE_LAYOUT,
'config.live.layout',
CONF_LIVE_LAYOUT_FIT,
CONF_LIVE_LAYOUT_POSITION_X,
CONF_LIVE_LAYOUT_POSITION_Y,
)}
</div>
`
: ''}
@@ -1327,44 +1390,53 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
true,
'config.media_viewer.controls.options',
{ name: 'mdi:gamepad' },
html` ${this._renderOptionSelector(
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
this._eventViewerNextPreviousControlStyles,
)}
${this._renderNumberInput(
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
{
min: BUTTON_SIZE_MIN,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
this._thumbnailModes,
)}
${this._renderNumberInput(CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE, {
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
})}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
defaults.media_viewer.controls.thumbnails.show_details,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
defaults.media_viewer.controls.thumbnails.show_favorite_control,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
defaults.media_viewer.controls.thumbnails.show_timeline_control,
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
this._titleModes,
)}
${this._renderNumberInput(
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
{ min: 0, max: 60 },
)}`,
html`
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_STYLE,
this._eventViewerNextPreviousControlStyles,
)}
${this._renderNumberInput(
CONF_MEDIA_VIEWER_CONTROLS_NEXT_PREVIOUS_SIZE,
{
min: BUTTON_SIZE_MIN,
},
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_MODE,
this._thumbnailModes,
)}
${this._renderNumberInput(CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SIZE, {
min: THUMBNAIL_WIDTH_MIN,
max: THUMBNAIL_WIDTH_MAX,
})}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_DETAILS,
defaults.media_viewer.controls.thumbnails.show_details,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_FAVORITE_CONTROL,
defaults.media_viewer.controls.thumbnails.show_favorite_control,
)}
${this._renderSwitch(
CONF_MEDIA_VIEWER_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
defaults.media_viewer.controls.thumbnails.show_timeline_control,
)}
${this._renderOptionSelector(
CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE,
this._titleModes,
)}
${this._renderNumberInput(
CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS,
{ min: 0, max: 60 },
)}
`,
)}
${this._renderMediaLayout(
MENU_MEDIA_VIEWER_LAYOUT,
'config.media_viewer.layout',
CONF_MEDIA_VIEWER_LAYOUT_FIT,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_X,
CONF_MEDIA_VIEWER_LAYOUT_POSITION_Y,
)}
</div>`
: ''}
@@ -1374,6 +1446,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderOptionSelector(CONF_IMAGE_MODE, this._imageModes)}
${this._renderStringInput(CONF_IMAGE_URL)}
${this._renderNumberInput(CONF_IMAGE_REFRESH_SECONDS)}
${this._renderMediaLayout(
MENU_IMAGE_LAYOUT,
'config.image.layout',
CONF_IMAGE_LAYOUT_FIT,
CONF_IMAGE_LAYOUT_POSITION_X,
CONF_IMAGE_LAYOUT_POSITION_Y,
)}
</div>`
: ''}
${this._renderOptionSetHeader('timeline')}
+15
View File
@@ -56,6 +56,18 @@
"selected": "On selection",
"unselected": "On unselection",
"visible": "On browser/tab visibility"
},
"layout": {
"fit": "Layout fit",
"fits": {
"cover": "Media expands proportionally to cover the card",
"contain": "Media is contained/letterboxed",
"fill": "Media is stretched to fill the card"
},
"position": {
"x": "Horizontal placement percentage",
"y": "Vertical placement percentage"
}
}
},
"dimensions": {
@@ -79,6 +91,7 @@
}
},
"image": {
"layout": "Image Layout",
"mode": "Image view mode",
"modes": {
"camera": "Home Assistant camera snapshot of camera entity",
@@ -122,6 +135,7 @@
}
},
"draggable": "Live cameras view can be dragged/swiped",
"layout": "Live Layout",
"lazy_load": "Live cameras are lazily loaded",
"lazy_unload": "Live cameras are lazily unloaded",
"preload": "Preload live view in the background",
@@ -172,6 +186,7 @@
},
"draggable": "Media Viewer can be dragged/swiped",
"lazy_load": "Media Viewer media is lazily loaded in carousel",
"layout": "Media Viewer Layout",
"transition_effect": "Media Viewer transition effect",
"transition_effects": {
"none": "No transition",
+11 -10
View File
@@ -9,13 +9,14 @@
// available as compilation time.
// ====================================================================
import { css, html, TemplateResult } from 'lit';
import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { query } from 'lit/decorators/query.js';
import { FrigateCardMediaPlayer } from '../types.js';
import { dispatchMediaShowEvent } from '../utils/media-info.js';
import "./ha-hls-player";
import "./ha-web-rtc-player";
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
import './ha-hls-player';
import './ha-web-rtc-player';
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
customElements.whenDefined('ha-camera-stream').then(() => {
// ========================================================================================
@@ -98,8 +99,8 @@ customElements.whenDefined('ha-camera-stream').then(() => {
if (this._shouldRenderMJPEG) {
return html`
<img
@load=${(e) => {
dispatchMediaShowEvent(this, e);
@load=${(ev: Event) => {
dispatchMediaLoadedEvent(this, ev);
}}
.src=${typeof this._connected == 'undefined' || this._connected
? computeMJPEGStreamUrl(this.stateObj)
@@ -138,6 +139,7 @@ customElements.whenDefined('ha-camera-stream').then(() => {
static get styles(): CSSResultGroup {
return [
super.styles,
unsafeCSS(liveHAComponentsStyle),
css`
:host {
width: 100%;
@@ -146,7 +148,6 @@ customElements.whenDefined('ha-camera-stream').then(() => {
img {
width: 100%;
height: 100%;
object-fit: contain;
}
`,
];
@@ -155,7 +156,7 @@ customElements.whenDefined('ha-camera-stream').then(() => {
});
declare global {
interface HTMLElementTagNameMap {
"frigate-card-ha-camera-stream": FrigateCardHaCameraStream
}
interface HTMLElementTagNameMap {
'frigate-card-ha-camera-stream': FrigateCardHaCameraStream;
}
}
+6 -5
View File
@@ -9,11 +9,12 @@
// available as compilation time.
// ====================================================================
import { css, html, TemplateResult } from 'lit';
import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { query } from 'lit/decorators/query.js';
import { dispatchErrorMessageEvent } from '../components/message.js';
import { dispatchMediaShowEvent } from '../utils/media-info.js';
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
customElements.whenDefined('ha-hls-player').then(() => {
@customElement('frigate-card-ha-hls-player')
@@ -85,7 +86,7 @@ customElements.whenDefined('ha-hls-player').then(() => {
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${(e) => {
dispatchMediaShowEvent(this, e);
dispatchMediaLoadedEvent(this, e);
}}
></video>
`;
@@ -94,15 +95,15 @@ customElements.whenDefined('ha-hls-player').then(() => {
static get styles(): CSSResultGroup {
return [
super.styles,
unsafeCSS(liveHAComponentsStyle),
css`
:host {
width: 100%;
height: 100%;
}
video {
object-fit: contain;
height: 100%;
width: 100%;
height: 100%;
}
`,
];
+21 -3
View File
@@ -9,11 +9,12 @@
// available as compilation time.
// ====================================================================
import { html, TemplateResult } from 'lit';
import { css, CSSResultGroup, html, unsafeCSS, TemplateResult } from 'lit';
import { customElement } from 'lit/decorators.js';
import { query } from 'lit/decorators/query.js';
import { dispatchErrorMessageEvent } from '../components/message.js';
import { dispatchMediaShowEvent } from '../utils/media-info.js';
import { dispatchMediaLoadedEvent } from '../utils/media-info.js';
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
customElements.whenDefined('ha-web-rtc-player').then(() => {
@customElement('frigate-card-ha-web-rtc-player')
@@ -86,11 +87,28 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
?playsinline=${this.playsInline}
?controls=${this.controls}
@loadeddata=${(e) => {
dispatchMediaShowEvent(this, e);
dispatchMediaLoadedEvent(this, e);
}}
></video>
`;
}
static get styles(): CSSResultGroup {
return [
super.styles,
unsafeCSS(liveHAComponentsStyle),
css`
:host {
width: 100%;
height: 100%;
}
video {
width: 100%;
height: 100%;
}
`,
];
}
}
});
+3 -1
View File
@@ -1,4 +1,6 @@
:host {
--frigate-card-thumbnail-size-max: 175px;
--frigate-card-thumbnail-details-width: calc(var(--frigate-card-thumbnail-size) + 200px);
--frigate-card-thumbnail-details-width: calc(
var(--frigate-card-thumbnail-size) + 200px
);
}
+5
View File
@@ -26,6 +26,11 @@
background: var(--secondary-background-color);
display: grid;
}
// Space submenus from the next top-level options.
.values + .option,
.submenu + .option {
margin-top: 10px;
}
div.upgrade {
width: auto;
border: 1px dotted var(--primary-color);
+4 -1
View File
@@ -1,6 +1,9 @@
@use 'media-layout.scss';
img {
width: 100%;
height: 100%;
display: block;
object-fit: scale-down;
@include media-layout.media-layout();
}
+5
View File
@@ -0,0 +1,5 @@
@use 'media-layout.scss';
img, video {
@include media-layout.media-layout();
}
+5 -2
View File
@@ -1,3 +1,5 @@
@use 'media-layout.scss';
:host {
width: 100%;
height: 100%;
@@ -8,5 +10,6 @@ canvas {
display: block;
width: 100%;
height: 100%;
object-fit: contain;
}
@include media-layout.media-layout();
}
+7 -1
View File
@@ -1,3 +1,5 @@
@use 'media-layout.scss';
:host {
width: 100%;
height: 100%;
@@ -12,4 +14,8 @@
// To get the WebRTC player to line up correctly with some themes.
margin: 0px;
box-shadow: none;
}
}
#webrtc #video {
@include media-layout.media-layout();
}
+5
View File
@@ -0,0 +1,5 @@
@mixin media-layout {
object-fit: var(--frigate-card-media-layout-fit, contain);
object-position: var(--frigate-card-media-layout-position-x, 50%)
var(--frigate-card-media-layout-position-y, 50%);
}
+4 -4
View File
@@ -1,3 +1,5 @@
@use 'media-layout.scss';
.embla__slide {
height: 100%;
flex: 0 0 100%;
@@ -8,7 +10,5 @@
width: 100%;
height: 100%;
// Letterbox media. <frigate-card-ha-hls-player> has similar added directly in
// its element.
object-fit: contain;
}
@include media-layout.media-layout();
}
+58 -30
View File
@@ -165,9 +165,11 @@ const moreInfoActionSchema = schemaForType<
const customActionSchema = schemaForType<
CustomActionConfig & ExtendedConfirmationRestrictionConfig
>()(
actionBaseSchema.extend({
action: z.literal('fire-dom-event'),
}).passthrough(),
actionBaseSchema
.extend({
action: z.literal('fire-dom-event'),
})
.passthrough(),
);
const noActionSchema = schemaForType<
NoActionConfig & ExtendedConfirmationRestrictionConfig
@@ -519,6 +521,18 @@ export type PictureElement = z.infer<typeof pictureElementSchema>;
const pictureElementsSchema = pictureElementSchema.array().optional();
export type PictureElements = z.infer<typeof pictureElementsSchema>;
/**
* Media layout configuration section.
*/
const mediaLayoutConfigSchema = z.object({
fit: z.enum(['contain', 'cover', 'fill']).optional(),
position: z.object({
x: z.number().min(0).max(100).optional(),
y: z.number().min(0).max(100).optional(),
}).optional(),
});
export type MediaLayoutConfig = z.infer<typeof mediaLayoutConfigSchema>;
/**
* View configuration section.
*/
@@ -580,6 +594,7 @@ const imageConfigSchema = z
mode: z.enum(IMAGE_MODES).default(imageConfigDefault.mode),
url: z.string().optional(),
refresh_seconds: z.number().min(0).default(imageConfigDefault.refresh_seconds),
layout: mediaLayoutConfigSchema.optional(),
})
.merge(actionsSchema)
.default(imageConfigDefault);
@@ -717,12 +732,14 @@ const liveOverridableConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
liveConfigDefault.controls.thumbnails.show_details,
),
show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
liveConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
liveConfigDefault.controls.thumbnails.show_timeline_control,
),
show_favorite_control:
thumbnailsControlSchema.shape.show_favorite_control.default(
liveConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control:
thumbnailsControlSchema.shape.show_timeline_control.default(
liveConfigDefault.controls.thumbnails.show_timeline_control,
),
media: z
.enum(['clips', 'snapshots'])
.default(liveConfigDefault.controls.thumbnails.media),
@@ -740,7 +757,10 @@ const liveOverridableConfigSchema = z
.default(liveConfigDefault.controls.title),
})
.default(liveConfigDefault.controls),
show_image_during_load: z.boolean().default(liveConfigDefault.show_image_during_load),
show_image_during_load: z
.boolean()
.default(liveConfigDefault.show_image_during_load),
layout: mediaLayoutConfigSchema.optional(),
})
.merge(actionsSchema);
@@ -917,12 +937,14 @@ const viewerConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
viewerConfigDefault.controls.thumbnails.show_details,
),
show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
viewerConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
viewerConfigDefault.controls.thumbnails.show_timeline_control,
),
show_favorite_control:
thumbnailsControlSchema.shape.show_favorite_control.default(
viewerConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control:
thumbnailsControlSchema.shape.show_timeline_control.default(
viewerConfigDefault.controls.thumbnails.show_timeline_control,
),
})
.default(viewerConfigDefault.controls.thumbnails),
title: titleControlConfigSchema
@@ -937,6 +959,7 @@ const viewerConfigSchema = z
.default(viewerConfigDefault.controls.title),
})
.default(viewerConfigDefault.controls),
layout: mediaLayoutConfigSchema.optional(),
})
.merge(actionsSchema)
.default(viewerConfigDefault);
@@ -970,12 +993,14 @@ const galleryConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
galleryConfigDefault.controls.thumbnails.show_details,
),
show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
galleryConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
galleryConfigDefault.controls.thumbnails.show_timeline_control,
),
show_favorite_control:
thumbnailsControlSchema.shape.show_favorite_control.default(
galleryConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control:
thumbnailsControlSchema.shape.show_timeline_control.default(
galleryConfigDefault.controls.thumbnails.show_timeline_control,
),
})
.default(galleryConfigDefault.controls.thumbnails),
})
@@ -1062,12 +1087,14 @@ const timelineConfigSchema = z
show_details: thumbnailsControlSchema.shape.show_details.default(
timelineConfigDefault.controls.thumbnails.show_details,
),
show_favorite_control: thumbnailsControlSchema.shape.show_favorite_control.default(
timelineConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control: thumbnailsControlSchema.shape.show_timeline_control.default(
timelineConfigDefault.controls.thumbnails.show_timeline_control,
),
show_favorite_control:
thumbnailsControlSchema.shape.show_favorite_control.default(
timelineConfigDefault.controls.thumbnails.show_favorite_control,
),
show_timeline_control:
thumbnailsControlSchema.shape.show_timeline_control.default(
timelineConfigDefault.controls.thumbnails.show_timeline_control,
),
})
.default(timelineConfigDefault.controls.thumbnails),
})
@@ -1086,6 +1113,7 @@ const overrideConfigurationSchema = z.object({
menu: deepRemoveDefaults(menuConfigSchema).optional(),
image: deepRemoveDefaults(imageConfigSchema).optional(),
view: deepRemoveDefaults(viewConfigSchema).optional(),
dimensions: deepRemoveDefaults(dimensionsConfigSchema).optional(),
});
export type OverrideConfigurationKey = keyof z.infer<typeof overrideConfigurationSchema>;
@@ -1213,13 +1241,13 @@ export const MESSAGE_TYPE_PRIORITIES = {
error: 20,
connection: 30,
diagnostics: 40,
}
};
export type MessageType = 'info' | 'error' | 'connection' | 'diagnostics';
export interface Message {
message: string;
type: MessageType,
type: MessageType;
icon?: string;
context?: unknown;
dotdotdot?: boolean;
+1 -1
View File
@@ -43,7 +43,7 @@ export function createMediaLoadedInfo(
* @param element The element to send the event.
* @param source An event or HTMLElement that should be used as a source.
*/
export function dispatchMediaShowEvent(
export function dispatchMediaLoadedEvent(
element: HTMLElement,
source: Event | HTMLElement,
): void {
+27
View File
@@ -0,0 +1,27 @@
import { MediaLayoutConfig } from '../types';
/**
* Update element style from a media configuration.
* @param element The element to update the style for.
* @param mediaLayoutConfig The media config object.
*/
export const updateElementStyleFromMediaLayoutConfig = (
element: HTMLElement,
mediaLayoutConfig?: MediaLayoutConfig,
) => {
if (mediaLayoutConfig?.fit !== undefined) {
element.style.setProperty('--frigate-card-media-layout-fit', mediaLayoutConfig.fit);
} else {
element.style.removeProperty('--frigate-card-media-layout-fit');
}
for (const dimension of ['x', 'y']) {
if (mediaLayoutConfig?.position?.[dimension] !== undefined) {
element.style.setProperty(
`--frigate-card-media-layout-position-${dimension}`,
`${mediaLayoutConfig.position[dimension]}%`,
);
} else {
element.style.removeProperty(`--frigate-card-media-layout-position-${dimension}`);
}
}
};