From 992b622f7c375e6e491eefc5262a09a2bc1915e6 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 6 May 2023 14:03:05 -0700 Subject: [PATCH] Add support for disabling zoom. --- README.md | 6 ++ src/components/image.ts | 18 ++-- src/components/live/live-image.ts | 4 + src/components/live/live.ts | 141 ++++++++++++++++-------------- src/components/viewer.ts | 131 ++++++++++++++------------- src/const.ts | 3 + src/editor.ts | 39 +++++---- src/localize/languages/en.json | 9 +- src/localize/languages/it.json | 9 +- src/localize/languages/pt-BR.json | 9 +- src/localize/languages/pt-PT.json | 9 +- src/types.ts | 6 ++ 12 files changed, 223 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 621a709f..73220225 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,7 @@ See the [fully expanded live configuration example](#config-expanded-live) for h | `lazy_load` | `true` | :heavy_multiplication_x: | Whether or not to lazily load cameras in the camera carousel. Setting this will `false` will cause all cameras to load simultaneously when the `live` carousel is opened (or cause all cameras to load continually if both `lazy_load` and `preload` are `true`). This will result in a smoother carousel experience at a cost of (potentially) a substantial amount of continually streamed data. | | `lazy_unload` | `never` | :heavy_multiplication_x: | When to lazily **un**load lazyily-loaded cameras. `never` will never lazily-unload, `unselected` will lazy-unload a camera when it is unselected in the carousel, `hidden` will lazy-unload all cameras when the browser/tab becomes hidden or `all` on any opportunity to lazily unload (i.e. either case). This will cause a reloading delay on revisiting that camera in the carousel but will save the streaming network resources that are otherwise consumed. This option has no effect if `lazy_load` is false. Some live providers (e.g. `webrtc-card`) implement their own lazy unloading independently which may occur regardless of the value of this setting.| | `draggable` | `true` | :heavy_multiplication_x: | Whether or not the live carousel can be dragged left or right, via touch/swipe and mouse dragging. | +| `zoomable` | `true` | :white_check_mark: | Whether or not the live carousel can be zoomed and panned, via touch/pinch and mouse scroll wheel with `ctrl` held. | | `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between live cameras. Accepted values: `slide` or `none`. | | `show_image_during_load` | `true` | :white_check_mark: | If `true`, during the initial stream load, the `image` live provider will be shown instead of the loading video stream. This still image will auto-refresh and is replaced with the live stream once loaded. | | `actions` | | :white_check_mark: | Actions to use for the `live` view. See [actions](#actions) below.| @@ -580,6 +581,7 @@ See the [fully expanded Media viewer configuration example](#config-expanded-med | `auto_unmute` | `never` | :heavy_multiplication_x: | Whether to automatically unmute events. `never` will never automatically unmute, `selected` will automatically unmute when an event is selected in the carousel, `visible` will automatically unmute when the browser/tab becomes visible or `all` on any opportunity to automatically unmute (i.e. either case). Note that some browsers will not allow automated unmute until the user has interacted with the page in some way -- if the user has not then the browser may pause the media instead.| | `lazy_load` | `true` | :heavy_multiplication_x: | Whether or not to lazily load media in the Media viewer carousel. Setting this will false will fetch all media immediately which may make the carousel experience smoother at a cost of (potentially) a substantial number of simultaneous media fetches on load. | | `draggable` | `true` | :heavy_multiplication_x: | Whether or not the Media viewer carousel can be dragged left or right, via touch/swipe and mouse dragging. | +| `zoomable` | `true` | :heavy_multiplication_x: | Whether or not the Media Viewer can be zoomed and panned, via touch/pinch and mouse scroll wheel with `ctrl` held. | | `snapshot_click_plays_clip` | `true` | :heavy_multiplication_x: | Whether clicking on a snapshot in the media viewer should play a related clip. | | `transition_effect` | `slide` | :heavy_multiplication_x: | Effect to apply as a transition between event media. Accepted values: `slide` or `none`. | | `controls` | | :heavy_multiplication_x: | Configuration for the Media viewer controls. See below. | @@ -722,6 +724,7 @@ See the [fully expanded image configuration example](#config-expanded-image) for | `mode` | `url` | :white_check_mark: | Mode of the the `image` [view](#views). Value must be one of `url` (to fetch an arbitrary image URL), `camera` (to show a still of the currently selected camera using either `camera_entity` or `webrtc_card.entity` in that order of precedence), or `screensaver` (to show an [embedded stock Frigate card logo](https://github.com/dermotduffy/frigate-hass-card/blob/main/src/images/frigate-bird-in-sky.jpg)). In either `url` or `camera` mode, the `screensaver` content is used as a fallback if a URL is not specified or cannot be derived. | | `url` | | :white_check_mark: | A static image URL to be used when the `mode` is set to `url` or when a temporary image is required (e.g. may appear momentarily prior to load of a camera snapshot in the `camera` mode). Note that a `_t=[timestsamp]` query parameter will be automatically added to all URLs such that the image will not be cached by the browser.| | `refresh_seconds` | 0 | :white_check_mark: | The image will be refreshed at least every `refresh_seconds` (it may refresh more frequently, e.g. whenever Home Assistant updates its camera security token). `0` implies no refreshing. | +| `zoomable` | `true` | :white_check_mark: | Whether or not the image can be zoomed and panned, via touch/pinch and mouse scroll wheel with `ctrl` held. | | `actions` | | :white_check_mark: | Actions to use for the `image` view. See [actions](#actions) below.| **Note**: When `mode` is set to `camera` this is effectively providing the same image as the `image` live provider would show in the live camera carousel. @@ -1844,6 +1847,7 @@ live: lazy_load: true lazy_unload: never draggable: true + zoomable: true transition_effect: slide controls: next_previous: @@ -1905,6 +1909,7 @@ media_viewer: auto_unmute: never lazy_load: true draggable: true + zoomable: true snapshot_click_plays_clip: true transition_effect: slide controls: @@ -1992,6 +1997,7 @@ Reference: [Image Options](#image-options). image: mode: url refresh_seconds: 0 + zoomable: true layout: fit: contain position: diff --git a/src/components/image.ts b/src/components/image.ts index 5860c92a..9a9f3e99 100644 --- a/src/components/image.ts +++ b/src/components/image.ts @@ -26,7 +26,6 @@ import { View } from '../view/view.js'; import { dispatchErrorMessageEvent } from './message.js'; import { contentsChanged } from '../utils/basic.js'; import isEqual from 'lodash-es/isEqual'; -import './zoomer.js'; // See TOKEN_CHANGE_INTERVAL in https://github.com/home-assistant/core/blob/dev/homeassistant/components/camera/__init__.py . const HASS_REJECTION_CUTOFF_MS = 5 * 60 * 1000; @@ -42,9 +41,6 @@ export class FrigateCardImage extends LitElement { @property({ attribute: false }) public cameraConfig?: CameraConfig; - @property({ attribute: false }) - public supportZoom = false; - // Using contentsChanged to ensure overridden configs (e.g. when the // 'show_image_during_load' option is true for live views, an overridden // config may be used here). @@ -115,6 +111,10 @@ export class FrigateCardImage extends LitElement { ); } updateElementStyleFromMediaLayoutConfig(this, this.imageConfig?.layout); + + if (changedProps.has('imageConfig') && this.imageConfig?.zoomable) { + import('./zoomer.js'); + } } // If the camera or view changed, immediately discard the old value (view to @@ -235,10 +235,10 @@ export class FrigateCardImage extends LitElement { } } - protected _renderZoom(contents: TemplateResult): TemplateResult { - return this.supportZoom - ? html` ${contents}` - : contents; + protected _useZoomIfRequired(template: TemplateResult): TemplateResult { + return this.imageConfig?.zoomable + ? html` ${template} ` + : template; } protected render(): TemplateResult | void { @@ -246,7 +246,7 @@ export class FrigateCardImage extends LitElement { // Note the use of live() below to ensure the update will restore the image // src if it's been changed via _forceSafeImage(). return src - ? this._renderZoom(html` { diff --git a/src/components/live/live-image.ts b/src/components/live/live-image.ts index ab8b5006..4bc38d15 100644 --- a/src/components/live/live-image.ts +++ b/src/components/live/live-image.ts @@ -59,6 +59,10 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia mode: this.cameraConfig.image.url ? ('url' as const) : ('camera' as const), refresh_seconds: this._playing ? this.cameraConfig.image.refresh_seconds : 0, url: this.cameraConfig.image.url, + + // The live provider will take care of zoom. + zoomable: false, + // Don't need to pass layout options as FrigateCardLiveProvider has // already taken care of this for us. }} diff --git a/src/components/live/live.ts b/src/components/live/live.ts index 6e68c64d..a5623dc3 100644 --- a/src/components/live/live.ts +++ b/src/components/live/live.ts @@ -54,7 +54,6 @@ import { dispatchErrorMessageEvent, dispatchMessageEvent } from '../message.js'; import '../next-prev-control.js'; import '../surround.js'; import '../title-control.js'; -import '../zoomer.js'; import { AutoMediaPlugin } from './../embla-plugins/automedia.js'; import { Lazyload } from './../embla-plugins/lazyload.js'; @@ -525,7 +524,12 @@ export class FrigateCardLiveCarousel extends LitElement { cameraConfig: CameraConfig, slideIndex: number, ): TemplateResult | void { - if (!this.liveConfig || !this.hass || !this.cameraManager || !this.conditionControllerEpoch) { + if ( + !this.liveConfig || + !this.hass || + !this.cameraManager || + !this.conditionControllerEpoch + ) { return; } // The condition controller object contains the currently live camera, which @@ -545,7 +549,9 @@ export class FrigateCardLiveCarousel extends LitElement {
this.setControls(false)} + @frigate-card:zoom:unzoomed=${() => this.setControls(true)} + > + ${template} + ` + : template; + } + /** * Master render method. * @returns A rendered template. @@ -894,42 +914,36 @@ export class FrigateCardLiveProvider hidden: showImageDuringLoading, }; - return html` - this.setControls(false)} - @frigate-card:zoom:unzoomed=${() => this.setControls(true)} - > - ${showImageDuringLoading || provider === 'image' - ? html` - { - if (provider === 'image') { - // Only count the media has loaded if the required provider is - // the image (not just the temporary image shown during - // loading). - this._videoMediaShowHandler(); - } else { - ev.stopPropagation(); - } - }} - > - ` - : html``} - ${provider === 'ha' - ? html` - ` - : provider === 'go2rtc' - ? html` { + if (provider === 'image') { + // Only count the media has loaded if the required provider is + // the image (not just the temporary image shown during + // loading). + this._videoMediaShowHandler(); + } else { + ev.stopPropagation(); + } + }} + > + ` + : html``} + ${provider === 'ha' + ? html` + ` + : provider === 'go2rtc' + ? html` ` - : provider === 'webrtc-card' - ? html` - ` - : provider === 'jsmpeg' - ? html` - ` - : html``} - - `; + : provider === 'webrtc-card' + ? html` + ` + : provider === 'jsmpeg' + ? html` + ` + : html``} + `); } static get styles(): CSSResultGroup { diff --git a/src/components/viewer.ts b/src/components/viewer.ts index 4746f052..49ae7e0d 100644 --- a/src/components/viewer.ts +++ b/src/components/viewer.ts @@ -47,7 +47,6 @@ import { MediaQueriesClassifier } from '../view/media-queries-classifier'; import { MediaQueriesResults } from '../view/media-queries-results.js'; import { VideoContentType, ViewMedia } from '../view/media.js'; import { View } from '../view/view.js'; -import './zoomer.js'; import type { CarouselSelect } from './carousel.js'; import { AutoMediaPlugin } from './embla-plugins/automedia.js'; import { Lazyload } from './embla-plugins/lazyload.js'; @@ -683,6 +682,21 @@ export class FrigateCardViewerProvider this.requestUpdate(); }); } + + if (changedProps.has('viewerConfig') && this.viewerConfig?.zoomable) { + import('./zoomer.js'); + } + } + + protected _useZoomIfRequired(template: TemplateResult): TemplateResult { + return this.viewerConfig?.zoomable + ? html` this.setControls(false)} + @frigate-card:zoom:unzoomed=${() => this.setControls(true)} + > + ${template} + ` + : template; } protected render(): TemplateResult | void { @@ -702,68 +716,63 @@ export class FrigateCardViewerProvider }); } - return html` - this.setControls(false)} - @frigate-card:zoom:unzoomed=${() => this.setControls(true)} - > - ${ViewMediaClassifier.isVideo(this.media) - ? this.media.getVideoContentType() === VideoContentType.HLS - ? html` - ` - : html` - - ` - : html` { - if (this.viewerConfig?.snapshot_click_plays_clip) { - this._dispatchRelatedClipView(); - } - }} - @load=${(e: Event) => { - dispatchMediaLoadedEvent(this, e); - }} - />`} - - `; + url=${canonicalizeHAURL(this.hass, resolvedMedia?.url) ?? ''} + .hass=${this.hass} + > + ` + : html` + + ` + : html` { + if (this.viewerConfig?.snapshot_click_plays_clip) { + this._dispatchRelatedClipView(); + } + }} + @load=${(e: Event) => { + dispatchMediaLoadedEvent(this, e); + }} + />`} + `); } static get styles(): CSSResultGroup { diff --git a/src/const.ts b/src/const.ts index c0df8745..d83bff3b 100644 --- a/src/const.ts +++ b/src/const.ts @@ -132,6 +132,7 @@ export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE = `${CONF_MEDIA_VIEWER}.controls.timeline.style` as const; export const CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS = `${CONF_MEDIA_VIEWER}.controls.timeline.window_seconds` as const; +export const CONF_MEDIA_VIEWER_ZOOMABLE = `${CONF_MEDIA_VIEWER}.zoomable` as const; export const CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE = `${CONF_MEDIA_VIEWER}.controls.title.mode` as const; @@ -193,6 +194,7 @@ export const CONF_LIVE_SHOW_IMAGE_DURING_LOAD = `${CONF_LIVE}.show_image_during_load` as const; export const CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS = `${CONF_LIVE}.microphone.disconnect_seconds` as const; +export const CONF_LIVE_ZOOMABLE = `${CONF_LIVE}.zoomable` as const; const CONF_IMAGE = 'image' as const; export const CONF_IMAGE_LAYOUT_FIT = `${CONF_IMAGE}.layout.fit` as const; @@ -201,6 +203,7 @@ export const CONF_IMAGE_LAYOUT_POSITION_Y = `${CONF_IMAGE}.layout.position.y` as 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; +export const CONF_IMAGE_ZOOMABLE = `${CONF_IMAGE}.zoomable` as const; const CONF_TIMELINE = 'timeline' as const; export const CONF_TIMELINE_WINDOW_SECONDS = `${CONF_TIMELINE}.window_seconds` as const; diff --git a/src/editor.ts b/src/editor.ts index ef6a1279..b76ebc07 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -2,6 +2,11 @@ import { fireEvent, HomeAssistant, LovelaceCardEditor } from 'custom-card-helper import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; +import { FRIGATE_ICON_SVG_PATH } from './camera-manager/frigate/icon.js'; +import { + MOTIONEYE_ICON_SVG_PATH, + MOTIONEYE_ICON_SVG_VIEWBOX, +} from './camera-manager/motioneye/icon.js'; import { copyConfig, deleteConfigValue, @@ -12,6 +17,7 @@ import { upgradeConfig, } from './config-mgmt.js'; import { + CONF_CAMERAS, CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS, CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS, @@ -39,15 +45,17 @@ import { CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY, CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY, CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, - CONF_CAMERAS, - CONF_DIMENSIONS_ASPECT_RATIO_MODE, CONF_DIMENSIONS_ASPECT_RATIO, + CONF_DIMENSIONS_ASPECT_RATIO_MODE, + CONF_DIMENSIONS_MAX_HEIGHT, + CONF_DIMENSIONS_MIN_HEIGHT, CONF_IMAGE_LAYOUT_FIT, CONF_IMAGE_LAYOUT_POSITION_X, CONF_IMAGE_LAYOUT_POSITION_Y, CONF_IMAGE_MODE, CONF_IMAGE_REFRESH_SECONDS, CONF_IMAGE_URL, + CONF_IMAGE_ZOOMABLE, CONF_LIVE_AUTO_MUTE, CONF_LIVE_AUTO_PAUSE, CONF_LIVE_AUTO_PLAY, @@ -65,6 +73,7 @@ import { CONF_LIVE_CONTROLS_TIMELINE_MEDIA, CONF_LIVE_CONTROLS_TIMELINE_MODE, CONF_LIVE_CONTROLS_TIMELINE_SHOW_RECORDINGS, + CONF_LIVE_CONTROLS_TIMELINE_STYLE, CONF_LIVE_CONTROLS_TIMELINE_WINDOW_SECONDS, CONF_LIVE_CONTROLS_TITLE_DURATION_SECONDS, CONF_LIVE_CONTROLS_TITLE_MODE, @@ -74,9 +83,11 @@ import { CONF_LIVE_LAYOUT_POSITION_Y, CONF_LIVE_LAZY_LOAD, CONF_LIVE_LAZY_UNLOAD, + CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS, CONF_LIVE_PRELOAD, CONF_LIVE_SHOW_IMAGE_DURING_LOAD, CONF_LIVE_TRANSITION_EFFECT, + CONF_LIVE_ZOOMABLE, CONF_MEDIA_GALLERY_CONTROLS_FILTER_MODE, CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DETAILS, CONF_MEDIA_GALLERY_CONTROLS_THUMBNAILS_SHOW_DOWNLOAD_CONTROL, @@ -99,6 +110,7 @@ import { CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MEDIA, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_MODE, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_SHOW_RECORDINGS, + CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE, CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_WINDOW_SECONDS, CONF_MEDIA_VIEWER_CONTROLS_TITLE_DURATION_SECONDS, CONF_MEDIA_VIEWER_CONTROLS_TITLE_MODE, @@ -109,6 +121,7 @@ import { CONF_MEDIA_VIEWER_LAZY_LOAD, CONF_MEDIA_VIEWER_SNAPSHOT_CLICK_PLAYS_CLIP, CONF_MEDIA_VIEWER_TRANSITION_EFFECT, + CONF_MEDIA_VIEWER_ZOOMABLE, CONF_MENU_ALIGNMENT, CONF_MENU_BUTTON_SIZE, CONF_MENU_BUTTONS, @@ -128,28 +141,24 @@ import { CONF_TIMELINE_CONTROLS_THUMBNAILS_SIZE, CONF_TIMELINE_MEDIA, CONF_TIMELINE_SHOW_RECORDINGS, + CONF_TIMELINE_STYLE, CONF_TIMELINE_WINDOW_SECONDS, CONF_VIEW_CAMERA_SELECT, CONF_VIEW_DARK_MODE, CONF_VIEW_DEFAULT, + CONF_VIEW_SCAN, CONF_VIEW_SCAN_ENABLED, CONF_VIEW_SCAN_SHOW_TRIGGER_STATUS, CONF_VIEW_SCAN_UNTRIGGER_RESET, CONF_VIEW_SCAN_UNTRIGGER_SECONDS, - CONF_VIEW_SCAN, CONF_VIEW_TIMEOUT_SECONDS, CONF_VIEW_UPDATE_CYCLE_CAMERA, CONF_VIEW_UPDATE_FORCE, CONF_VIEW_UPDATE_SECONDS, MEDIA_CHUNK_SIZE_MAX, - CONF_DIMENSIONS_MAX_HEIGHT, - CONF_DIMENSIONS_MIN_HEIGHT, - CONF_TIMELINE_STYLE, - CONF_LIVE_CONTROLS_TIMELINE_STYLE, - CONF_MEDIA_VIEWER_CONTROLS_TIMELINE_STYLE, - CONF_LIVE_MICROPHONE_DISCONNECT_SECONDS, } from './const.js'; import { localize } from './localize/localize.js'; +import { setLowPerformanceProfile } from './performance.js'; import frigate_card_editor_style from './scss/editor.scss'; import { BUTTON_SIZE_MIN, @@ -163,17 +172,11 @@ import { } from './types.js'; import { arrayMove, prettifyTitle } from './utils/basic.js'; import { getCameraID } from './utils/camera.js'; -import { FRIGATE_ICON_SVG_PATH } from './camera-manager/frigate/icon.js'; import { getEntitiesFromHASS, getEntityTitle, sideLoadHomeAssistantElements, } from './utils/ha'; -import { setLowPerformanceProfile } from './performance.js'; -import { - MOTIONEYE_ICON_SVG_PATH, - MOTIONEYE_ICON_SVG_VIEWBOX, -} from './camera-manager/motioneye/icon.js'; const MENU_BUTTONS = 'buttons'; const MENU_CAMERAS = 'cameras'; @@ -1770,6 +1773,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderSwitch(CONF_LIVE_PRELOAD, this._defaults.live.preload)} ${this._renderSwitch(CONF_LIVE_DRAGGABLE, this._defaults.live.draggable)} + ${this._renderSwitch(CONF_LIVE_ZOOMABLE, this._defaults.live.zoomable)} ${this._renderSwitch(CONF_LIVE_LAZY_LOAD, this._defaults.live.lazy_load)} ${this._renderOptionSelector( CONF_LIVE_LAZY_UNLOAD, @@ -1903,6 +1907,10 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor CONF_MEDIA_VIEWER_DRAGGABLE, this._defaults.media_viewer.draggable, )} + ${this._renderSwitch( + CONF_MEDIA_VIEWER_ZOOMABLE, + this._defaults.media_viewer.zoomable, + )} ${this._renderSwitch( CONF_MEDIA_VIEWER_LAZY_LOAD, this._defaults.media_viewer.lazy_load, @@ -1973,6 +1981,7 @@ 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._renderSwitch(CONF_IMAGE_ZOOMABLE, this._defaults.image.zoomable)} ${this._renderMediaLayout( MENU_IMAGE_LAYOUT, 'config.image.layout', diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 93759c24..a035bc09 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -204,7 +204,8 @@ "url": "Arbitrary image specified by URL" }, "refresh_seconds": "Number of seconds after which to refresh (0=never)", - "url": "Static image URL for image view" + "url": "Static image URL for image view", + "zoomable": "Image can be zoomed/panned" }, "live": { "auto_mute": "Automatically mute live cameras", @@ -225,7 +226,8 @@ }, "preload": "Preload live view in the background", "show_image_during_load": "Show still image while the live stream is loading", - "transition_effect": "Live camera transition effect" + "transition_effect": "Live camera transition effect", + "zoomable": "Live cameras can be zoomed/panned" }, "media_viewer": { "auto_mute": "Automatically mute media", @@ -243,7 +245,8 @@ "transition_effects": { "none": "No transition", "slide": "Slide transition" - } + }, + "zoomable": "Media Viewer can be zoomed/panned" }, "menu": { "alignment": "Menu alignment", diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index 13b7cdca..2a20f25c 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -204,7 +204,8 @@ "url": "Immagine arbitraria specificata dall'URL" }, "refresh_seconds": "Numero di secondi dopo i quali aggiornare (0 = mai)", - "url": "URL di immagine statica per la vista dell'immagine" + "url": "URL di immagine statica per la vista dell'immagine", + "zoomable": "" }, "live": { "auto_mute": "Muta automaticamente le telecamere in diretta", @@ -225,7 +226,8 @@ }, "preload": "Precarica Live View in background", "show_image_during_load": "Mostra un'immagine fissa durante il caricamento del live streaming", - "transition_effect": "Effetto di transizione della telecamera dal vivo" + "transition_effect": "Effetto di transizione della telecamera dal vivo", + "zoomable": "" }, "media_viewer": { "auto_mute": "Muta automaticamente i media", @@ -242,7 +244,8 @@ "transition_effects": { "none": "Nessuna transizione", "slide": "Transizione diapositiva" - } + }, + "zoomable": "" }, "menu": { "alignment": "Allineamento dei menu", diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index 553dff48..e9bda5c3 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -204,7 +204,8 @@ "url": "Imagem arbitrária especificada por URL" }, "refresh_seconds": "Número de segundos após o qual atualizar (0 = nunca)", - "url": "Imagem arbitrária especificada por URL" + "url": "Imagem arbitrária especificada por URL", + "zoomable": "" }, "live": { "auto_mute": "Silenciar câmeras ao vivo automaticamente", @@ -225,7 +226,8 @@ }, "preload": "Pré-carregar a visualização ao vivo em segundo plano", "show_image_during_load": "Mostrar imagem estática enquanto a transmissão ao vivo está carregando", - "transition_effect": "Efeito de transição de câmera ao vivo" + "transition_effect": "Efeito de transição de câmera ao vivo", + "zoomable": "" }, "media_viewer": { "auto_mute": "Silenciar mídia automaticamente", @@ -243,7 +245,8 @@ "transition_effects": { "none": "Sem transição", "slide": "Transição de slides" - } + }, + "zoomable": "" }, "menu": { "alignment": "Alinhamento do menu", diff --git a/src/localize/languages/pt-PT.json b/src/localize/languages/pt-PT.json index bbab7804..ca4fe617 100644 --- a/src/localize/languages/pt-PT.json +++ b/src/localize/languages/pt-PT.json @@ -197,7 +197,8 @@ "url": "Imagem arbitrária especificada por URL" }, "refresh_seconds": "Número de segundos após o qual atualizar (0 = nunca)", - "url": "Imagem arbitrária especificada por URL" + "url": "Imagem arbitrária especificada por URL", + "zoomable": "" }, "live": { "auto_mute": "Silenciar câmeras ao vivo automaticamente", @@ -213,7 +214,8 @@ "lazy_unload": "As câmeras ao vivo são descarregadas preguiçosamente", "preload": "Pré-carregar a visualização ao vivo em segundo plano", "show_image_during_load": "Mostar imagem durante o carregamento", - "transition_effect": "Efeito de transição de câmera ao vivo" + "transition_effect": "Efeito de transição de câmera ao vivo", + "zoomable": "" }, "media_viewer": { "auto_mute": "Silenciar mídia automaticamente", @@ -230,7 +232,8 @@ "transition_effects": { "none": "Sem transição", "slide": "Transição de slides" - } + }, + "zoomable": "" }, "menu": { "alignment": "Alinhamento do menu", diff --git a/src/types.ts b/src/types.ts index 5b9b065d..5580f896 100644 --- a/src/types.ts +++ b/src/types.ts @@ -783,12 +783,14 @@ const viewConfigSchema = z const IMAGE_MODES = ['screensaver', 'camera', 'url'] as const; const imageConfigDefault = { mode: 'url' as const, + zoomable: true, ...imageBaseConfigDefault, }; const imageConfigSchema = imageBaseConfigSchema .extend({ mode: z.enum(IMAGE_MODES).default(imageConfigDefault.mode), layout: mediaLayoutConfigSchema.optional(), + zoomable: z.boolean().default(imageConfigDefault.zoomable), }) .merge(actionsSchema) .default(imageConfigDefault); @@ -936,6 +938,7 @@ const liveConfigDefault = { lazy_load: true, lazy_unload: 'never' as const, draggable: true, + zoomable: true, transition_effect: 'slide' as const, show_image_during_load: true, controls: { @@ -997,6 +1000,7 @@ const liveOverridableConfigSchema = z .default(liveConfigDefault.show_image_during_load), layout: mediaLayoutConfigSchema.optional(), microphone: microphoneConfigSchema.default(liveConfigDefault.microphone), + zoomable: z.boolean().default(liveConfigDefault.zoomable), }) .merge(actionsSchema); @@ -1115,6 +1119,7 @@ const viewerConfigDefault = { auto_unmute: 'never' as const, lazy_load: true, draggable: true, + zoomable: true, transition_effect: 'slide' as const, snapshot_click_plays_clip: true, controls: { @@ -1155,6 +1160,7 @@ const viewerConfigSchema = z .default(viewerConfigDefault.auto_unmute), lazy_load: z.boolean().default(viewerConfigDefault.lazy_load), draggable: z.boolean().default(viewerConfigDefault.draggable), + zoomable: z.boolean().default(viewerConfigDefault.zoomable), transition_effect: transitionEffectConfigSchema.default( viewerConfigDefault.transition_effect, ),