@@ -37,7 +37,7 @@ menu:
|
||||
| `download` | The `download` menu button: allow direct download of the media being displayed. |
|
||||
| `expand` | The `expand` menu button: expand the card into a popup/dialog. |
|
||||
| `frigate` | The `Frigate` menu button: brings the user to the default configured view (`view.default`), or collapses/expands the menu if the `menu.style` is `hidden` . |
|
||||
| `fullscreen` | The `fullscreen` menu button: expand the card to consume the fullscreen. |
|
||||
| `fullscreen` | The `fullscreen` menu button: expand the card to consume the fullscreen. Please note that fullscreen behavior on iPhone is limited, see [troubleshooting](../troubleshooting.md?id=fullscreen-doesn39t-work-on-iphone). |
|
||||
| `image` | The `image` view menu button: brings the user to the static `image` view. |
|
||||
| `live` | The `live` view menu button: brings the user to the `live` view. |
|
||||
| `media_player` | The `media_player` menu button: sends the visible media to a remote media player. Supports Frigate clips, snapshots and live camera (only for cameras that specify a `camera_entity` and only using the default HA stream (equivalent to the `ha` live provider)). `jsmpeg` or `webrtc-card` are not supported, although live can still be played as long as `camera_entity` is specified. In the player list, a `tap` will send the media to the player, a `hold` will stop the media on the player. |
|
||||
|
||||
@@ -189,8 +189,15 @@ live:
|
||||
### Fullscreen doesn't work on iPhone
|
||||
|
||||
Unfortunately, [iOS does not support the Javascript fullscreen
|
||||
API](https://caniuse.com/fullscreen). As a result, card-level fullscreen support
|
||||
for the iPhone is not currently possible.
|
||||
API](https://caniuse.com/fullscreen) on the iPhone, which severely limits the
|
||||
fullscreen functionality available. On iPhone, fullscreen is only possible of
|
||||
the selected video element. As a result, there will be no menu, status bar, grid
|
||||
support, gallery / timeline support, nor support for non-video based [live
|
||||
providers](./configuration/cameras/live-provider.md) such as `image` or `jsmpeg`
|
||||
-- exclusively viewing a selected live video or media video in fullscreen.
|
||||
|
||||
The card will only show the fullscreen menu button when fullscreen can usefully
|
||||
be activated, which means for certain views on the iPhone it will be absent.
|
||||
|
||||
### iOS App not updating after card version change
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export class CardElementManager {
|
||||
);
|
||||
|
||||
this._api.getConditionsManager()?.setState({
|
||||
user_agent: navigator.userAgent,
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
|
||||
// Manually request query string execute as the card will be
|
||||
|
||||
@@ -17,25 +17,25 @@ import {
|
||||
ViewDisplayMode,
|
||||
} from '../config/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { FrigateCardError } from '../types';
|
||||
import { FrigateCardError, MediaLoadedInfo } from '../types';
|
||||
import { desparsifyArrays } from '../utils/basic';
|
||||
import { isCompanionApp } from '../utils/companion';
|
||||
import { CardConditionAPI, KeysState, MicrophoneState } from './types';
|
||||
|
||||
interface ConditionState {
|
||||
view?: string;
|
||||
fullscreen?: boolean;
|
||||
expand?: boolean;
|
||||
export interface ConditionState {
|
||||
camera?: string;
|
||||
state?: HassEntities;
|
||||
media_loaded?: boolean;
|
||||
displayMode?: ViewDisplayMode;
|
||||
triggered?: Set<string>;
|
||||
expand?: boolean;
|
||||
fullscreen?: boolean;
|
||||
interaction?: boolean;
|
||||
microphone?: MicrophoneState;
|
||||
user?: CurrentUser;
|
||||
keys?: KeysState;
|
||||
user_agent?: string;
|
||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
microphone?: MicrophoneState;
|
||||
state?: HassEntities;
|
||||
triggered?: Set<string>;
|
||||
user?: CurrentUser;
|
||||
userAgent?: string;
|
||||
view?: string;
|
||||
}
|
||||
|
||||
class OverrideConfigurationError extends FrigateCardError {}
|
||||
@@ -152,7 +152,10 @@ export interface ConditionsManagerEpoch {
|
||||
manager: Readonly<ConditionsManager>;
|
||||
}
|
||||
|
||||
export type ConditionsManagerListener = () => void;
|
||||
export type ConditionsManagerListener = (
|
||||
newState: ConditionState,
|
||||
oldState?: ConditionState,
|
||||
) => void;
|
||||
|
||||
export class ConditionsManager {
|
||||
protected _api: CardConditionAPI;
|
||||
@@ -166,7 +169,7 @@ export class ConditionsManager {
|
||||
// the configuration needs to consume it.
|
||||
protected _hasHAStateConditions = false;
|
||||
protected _mediaQueries: MediaQueryList[] = [];
|
||||
protected _mediaQueryTrigger = () => this._triggerChange();
|
||||
protected _mediaQueryTrigger = () => this._triggerChange(this._state);
|
||||
|
||||
constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) {
|
||||
this._api = api;
|
||||
@@ -177,6 +180,14 @@ export class ConditionsManager {
|
||||
];
|
||||
}
|
||||
|
||||
public addListener(listener: ConditionsManagerListener): void {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
|
||||
public removeListener(listener: ConditionsManagerListener): void {
|
||||
this._listeners = this._listeners.filter((l) => l !== listener);
|
||||
}
|
||||
|
||||
public removeConditions(): void {
|
||||
this._mediaQueries.forEach((mql) =>
|
||||
mql.removeEventListener('change', this._mediaQueryTrigger),
|
||||
@@ -233,12 +244,12 @@ export class ConditionsManager {
|
||||
if (Object.keys(state).every((key) => isEqual(state[key], this._state[key]))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = this._state;
|
||||
this._state = {
|
||||
...this._state,
|
||||
...oldState,
|
||||
...state,
|
||||
};
|
||||
this._triggerChange();
|
||||
this._triggerChange(oldState);
|
||||
}
|
||||
|
||||
public getState(): ConditionState {
|
||||
@@ -313,8 +324,8 @@ export class ConditionsManager {
|
||||
return !!state.user && conditionObj.users.includes(state.user.id);
|
||||
case 'media_loaded':
|
||||
return (
|
||||
state.media_loaded !== undefined &&
|
||||
conditionObj.media_loaded === state.media_loaded
|
||||
state.mediaLoadedInfo !== undefined &&
|
||||
conditionObj.media_loaded === !!state.mediaLoadedInfo
|
||||
);
|
||||
case 'screen':
|
||||
return window.matchMedia(conditionObj.media_query).matches;
|
||||
@@ -352,12 +363,12 @@ export class ConditionsManager {
|
||||
);
|
||||
case 'user_agent':
|
||||
return (
|
||||
!!state.user_agent &&
|
||||
(!conditionObj.user_agent || conditionObj.user_agent === state.user_agent) &&
|
||||
!!state.userAgent &&
|
||||
(!conditionObj.user_agent || conditionObj.user_agent === state.userAgent) &&
|
||||
(conditionObj.companion === undefined ||
|
||||
conditionObj.companion === isCompanionApp(state.user_agent)) &&
|
||||
conditionObj.companion === isCompanionApp(state.userAgent)) &&
|
||||
(conditionObj.user_agent_re === undefined ||
|
||||
new RegExp(conditionObj.user_agent_re).test(state.user_agent))
|
||||
new RegExp(conditionObj.user_agent_re).test(state.userAgent))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -366,8 +377,8 @@ export class ConditionsManager {
|
||||
return { manager: this };
|
||||
}
|
||||
|
||||
protected _triggerChange(): void {
|
||||
protected _triggerChange(oldState?: ConditionState): void {
|
||||
this._epoch = this._createEpoch();
|
||||
this._listeners.forEach((listener) => listener());
|
||||
this._listeners.forEach((listener) => listener(this._state, oldState));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { ConfigManager } from './config/config-manager';
|
||||
import { DefaultManager } from './default-manager';
|
||||
import { DownloadManager } from './download-manager';
|
||||
import { ExpandManager } from './expand-manager';
|
||||
import { FullscreenManager } from './fullscreen-manager';
|
||||
import { FullscreenManager } from './fullscreen/fullscreen-manager';
|
||||
import { HASSManager } from './hass/hass-manager';
|
||||
import { InitializationManager } from './initialization-manager';
|
||||
import { InteractionManager } from './interaction-manager';
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ExpandManager {
|
||||
public setExpanded(expanded: boolean): void {
|
||||
if (expanded && this._api.getFullscreenManager().isInFullscreen()) {
|
||||
// Fullscreen and expanded mode are mutually exclusive.
|
||||
this._api.getFullscreenManager().stopFullscreen();
|
||||
this._api.getFullscreenManager().setFullscreen(false);
|
||||
}
|
||||
|
||||
this._expanded = expanded;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { CardFullscreenAPI } from './types';
|
||||
|
||||
export class FullscreenManager {
|
||||
protected _api: CardFullscreenAPI;
|
||||
|
||||
constructor(api: CardFullscreenAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this._setConditionState();
|
||||
}
|
||||
|
||||
public connect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on('change', this._fullscreenHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.off('change', this._fullscreenHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return screenfull.isEnabled && screenfull.isFullscreen;
|
||||
}
|
||||
|
||||
public toggleFullscreen(): void {
|
||||
screenfull.toggle(this._api.getCardElementManager().getElement());
|
||||
}
|
||||
|
||||
public stopFullscreen(): void {
|
||||
screenfull.exit();
|
||||
}
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager()?.setState({
|
||||
fullscreen: this.isInFullscreen(),
|
||||
});
|
||||
}
|
||||
|
||||
protected _fullscreenHandler = (): void => {
|
||||
this._api.getExpandManager().setExpanded(false);
|
||||
|
||||
this._setConditionState();
|
||||
|
||||
// Re-render after a change to fullscreen mode to take advantage of
|
||||
// the expanded screen real-estate (vs staying in aspect-ratio locked
|
||||
// modes).
|
||||
this._api.getCardElementManager().update();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { WebkitHTMLVideoElement } from '../../types';
|
||||
import { CardFullscreenAPI } from '../types';
|
||||
import { ScreenfullFullScreenProvider } from './screenfull';
|
||||
import { FullscreenHandler, FullscreenProvider } from './types';
|
||||
import { WebkitFullScreenProvider } from './webkit';
|
||||
|
||||
export class FullscreenProviderFactory {
|
||||
public static create(
|
||||
api: CardFullscreenAPI,
|
||||
handler: FullscreenHandler,
|
||||
): FullscreenProvider | null {
|
||||
if (screenfull.isEnabled) {
|
||||
return new ScreenfullFullScreenProvider(api, handler);
|
||||
}
|
||||
|
||||
const video = document.createElement('video') as Partial<WebkitHTMLVideoElement>;
|
||||
if (!!video.webkitEnterFullscreen) {
|
||||
return new WebkitFullScreenProvider(api, handler);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { isBeingCasted } from '../../utils/casting';
|
||||
import { CardFullscreenAPI } from '../types';
|
||||
import { FullscreenProviderFactory } from './factory';
|
||||
import { FullscreenProvider } from './types';
|
||||
|
||||
export class FullscreenManager {
|
||||
protected _api: CardFullscreenAPI;
|
||||
protected _provider: FullscreenProvider | null;
|
||||
|
||||
constructor(api: CardFullscreenAPI, provider?: FullscreenProvider) {
|
||||
this._api = api;
|
||||
this._provider =
|
||||
provider ?? FullscreenProviderFactory.create(api, this._fullscreenHandler);
|
||||
}
|
||||
|
||||
public connect(): void {
|
||||
this._provider?.connect();
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this._provider?.disconnect();
|
||||
}
|
||||
|
||||
public setFullscreen(fullscreen: boolean): void {
|
||||
this._provider?.setFullscreen(fullscreen);
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return this._provider?.isInFullscreen() ?? false;
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
if (isBeingCasted()) {
|
||||
return false;
|
||||
}
|
||||
return this._provider?.isSupported() ?? false;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this._setConditionState();
|
||||
}
|
||||
|
||||
public toggleFullscreen(): void {
|
||||
if (this._provider?.isInFullscreen()) {
|
||||
this._provider.setFullscreen(false);
|
||||
} else {
|
||||
this._provider?.setFullscreen(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected _fullscreenHandler = (): void => {
|
||||
this._api.getExpandManager().setExpanded(false);
|
||||
|
||||
this._setConditionState();
|
||||
|
||||
// Re-render after a change to fullscreen mode to take advantage of
|
||||
// the expanded screen real-estate (vs staying in aspect-ratio locked
|
||||
// modes).
|
||||
this._api.getCardElementManager().update();
|
||||
};
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager()?.setState({
|
||||
fullscreen: this.isInFullscreen(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CardFullscreenAPI } from '../types';
|
||||
import { FullscreenHandler } from './types';
|
||||
|
||||
export class FullscreenProviderBase {
|
||||
protected _api: CardFullscreenAPI;
|
||||
protected _handler: FullscreenHandler;
|
||||
|
||||
constructor(api: CardFullscreenAPI, handler: FullscreenHandler) {
|
||||
this._api = api;
|
||||
this._handler = handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { FullscreenProviderBase } from '../provider';
|
||||
import { FullscreenProvider } from '../types';
|
||||
|
||||
export class ScreenfullFullScreenProvider
|
||||
extends FullscreenProviderBase
|
||||
implements FullscreenProvider
|
||||
{
|
||||
public connect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on('change', this._handler);
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.off('change', this._handler);
|
||||
}
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return screenfull.isEnabled && screenfull.isFullscreen;
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
return screenfull.isEnabled;
|
||||
}
|
||||
|
||||
public setFullscreen(fullscreen: boolean): void {
|
||||
if (!this.isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fullscreen) {
|
||||
screenfull.request(this._api.getCardElementManager().getElement());
|
||||
} else {
|
||||
screenfull.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export type FullscreenHandler = () => void;
|
||||
|
||||
export interface FullscreenProvider {
|
||||
connect(): void;
|
||||
disconnect(): void;
|
||||
|
||||
isInFullscreen(): boolean;
|
||||
isSupported(): boolean;
|
||||
|
||||
setFullscreen(fullscreen: boolean): void;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { WebkitHTMLVideoElement } from '../../../types';
|
||||
import { Timer } from '../../../utils/timer';
|
||||
import { ConditionState } from '../../conditions-manager';
|
||||
import { FullscreenProviderBase } from '../provider';
|
||||
import { FullscreenProvider } from '../types';
|
||||
|
||||
const WEBKIT_PLAY_SECONDS = 0.5;
|
||||
|
||||
/**
|
||||
* Fullscreen implementation for webkit based browsers that do not support the
|
||||
* standard Fullscreen API (expected to be exclusively iOS on an iPhone).
|
||||
* See: https://github.com/dermotduffy/frigate-hass-card/issues/1444
|
||||
*/
|
||||
export class WebkitFullScreenProvider
|
||||
extends FullscreenProviderBase
|
||||
implements FullscreenProvider
|
||||
{
|
||||
protected _playTimer = new Timer();
|
||||
|
||||
public connect(): void {
|
||||
this._api.getConditionsManager().addListener(this._conditionChangeHandler);
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this._api.getConditionsManager().removeListener(this._conditionChangeHandler);
|
||||
}
|
||||
|
||||
protected _conditionChangeHandler = (
|
||||
newState: ConditionState,
|
||||
oldState?: ConditionState,
|
||||
): void => {
|
||||
if (
|
||||
newState.mediaLoadedInfo?.player?.getFullscreenElement() !==
|
||||
oldState?.mediaLoadedInfo?.player?.getFullscreenElement()
|
||||
) {
|
||||
const oldElement = oldState?.mediaLoadedInfo?.player?.getFullscreenElement();
|
||||
oldElement?.removeEventListener('webkitbeginfullscreen', this._handler);
|
||||
oldElement?.removeEventListener('webkitendfullscreen', this._endHandler);
|
||||
|
||||
const newElement = newState.mediaLoadedInfo?.player?.getFullscreenElement();
|
||||
newElement?.addEventListener('webkitbeginfullscreen', this._handler);
|
||||
newElement?.addEventListener('webkitendfullscreen', this._endHandler);
|
||||
}
|
||||
};
|
||||
|
||||
protected _getVideoElement():
|
||||
| (HTMLVideoElement & Partial<WebkitHTMLVideoElement>)
|
||||
| null {
|
||||
const element = this._api
|
||||
.getMediaLoadedInfoManager()
|
||||
.get()
|
||||
?.player?.getFullscreenElement();
|
||||
return element instanceof HTMLVideoElement ? element : null;
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return !!this._getVideoElement()?.webkitDisplayingFullscreen;
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
return !!this._getVideoElement()?.webkitSupportsFullscreen;
|
||||
}
|
||||
|
||||
public setFullscreen(fullscreen: boolean): void {
|
||||
if (!this.isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const video = this._getVideoElement();
|
||||
|
||||
if (fullscreen) {
|
||||
video?.webkitEnterFullscreen?.();
|
||||
} else {
|
||||
video?.webkitExitFullscreen?.();
|
||||
}
|
||||
}
|
||||
|
||||
protected _endHandler = (): void => {
|
||||
this._handler();
|
||||
|
||||
// Webkit on iPhone stops the video when exiting fullscreen (why!). This
|
||||
// hack workaround has the disadvantage that there's no way to tell if the
|
||||
// video was legitimately paused before they user exited fullscreen (as the
|
||||
// common path is for exit fullscreen to not involve
|
||||
// setFullscreen(...) being called at all, but rather the user simply
|
||||
// clicking the 'X' which then fires this event). That's probably the rare
|
||||
// case though.
|
||||
this._playTimer.start(WEBKIT_PLAY_SECONDS, () => {
|
||||
this._getVideoElement()?.play();
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -16,21 +16,21 @@ export class MediaLoadedInfoManager {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public set(mediaInfo: MediaLoadedInfo): void {
|
||||
if (!isValidMediaLoadedInfo(mediaInfo)) {
|
||||
public set(mediaLoadedInfo: MediaLoadedInfo): void {
|
||||
if (!isValidMediaLoadedInfo(mediaLoadedInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(
|
||||
this._api.getConfigManager().getCardWideConfig(),
|
||||
`Frigate Card media load: `,
|
||||
mediaInfo,
|
||||
mediaLoadedInfo,
|
||||
);
|
||||
|
||||
this._current = mediaInfo;
|
||||
this._lastKnown = mediaInfo;
|
||||
this._current = mediaLoadedInfo;
|
||||
this._lastKnown = mediaLoadedInfo;
|
||||
|
||||
this._api.getConditionsManager().setState({ media_loaded: true });
|
||||
this._api.getConditionsManager().setState({ mediaLoadedInfo: mediaLoadedInfo });
|
||||
|
||||
// Fresh media information may change how the card is rendered.
|
||||
this._api.getStyleManager().setExpandedMode();
|
||||
@@ -47,7 +47,7 @@ export class MediaLoadedInfoManager {
|
||||
|
||||
public clear(): void {
|
||||
this._current = null;
|
||||
this._api.getConditionsManager().setState({ media_loaded: false });
|
||||
this._api.getConditionsManager().setState({ mediaLoadedInfo: null });
|
||||
}
|
||||
|
||||
public has(): boolean {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ConfigManager } from './config/config-manager';
|
||||
import type { DefaultManager } from './default-manager';
|
||||
import type { DownloadManager } from './download-manager';
|
||||
import type { ExpandManager } from './expand-manager';
|
||||
import type { FullscreenManager } from './fullscreen-manager';
|
||||
import type { FullscreenManager } from './fullscreen/fullscreen-manager';
|
||||
import type { HASSManager } from './hass/hass-manager';
|
||||
import type { InitializationManager } from './initialization-manager';
|
||||
import type { InteractionManager } from './interaction-manager';
|
||||
@@ -150,6 +150,7 @@ export interface CardFullscreenAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getExpandManager(): ExpandManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
getMediaPlayerManager(): MediaPlayerManager;
|
||||
}
|
||||
|
||||
@@ -205,6 +206,7 @@ export interface CardMediaLoadedAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getFullscreenManager(): FullscreenManager;
|
||||
getStyleManager(): StyleManager;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -270,7 +270,7 @@ class FrigateCard extends LitElement {
|
||||
this._controller.getCameraManager(),
|
||||
{
|
||||
inExpandedMode: this._controller.getExpandManager().isExpanded(),
|
||||
inFullscreenMode: this._controller.getFullscreenManager().isInFullscreen(),
|
||||
fullscreenManager: this._controller.getFullscreenManager(),
|
||||
currentMediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
||||
showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(),
|
||||
mediaPlayerController: this._controller.getMediaPlayerManager(),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import { StyleInfo } from 'lit/directives/style-map';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { FullscreenManager } from '../card-controller/fullscreen/fullscreen-manager';
|
||||
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
||||
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||
import { ViewManager } from '../card-controller/view/view-manager';
|
||||
@@ -32,7 +33,7 @@ import { getCameraIDsForViewName } from '../view/view-to-cameras';
|
||||
export interface MenuButtonControllerOptions {
|
||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
showCameraUIButton?: boolean;
|
||||
inFullscreenMode?: boolean;
|
||||
fullscreenManager?: FullscreenManager | null;
|
||||
inExpandedMode?: boolean;
|
||||
microphoneManager?: MicrophoneManager | null;
|
||||
mediaPlayerController?: MediaPlayerManager | null;
|
||||
@@ -84,7 +85,7 @@ export class MenuButtonController {
|
||||
options?.currentMediaLoadedInfo,
|
||||
),
|
||||
this._getExpandButton(config, options?.inExpandedMode),
|
||||
this._getFullscreenButton(config, options?.inFullscreenMode),
|
||||
this._getFullscreenButton(config, options?.fullscreenManager),
|
||||
this._getCastButton(
|
||||
hass,
|
||||
config,
|
||||
@@ -420,16 +421,17 @@ export class MenuButtonController {
|
||||
|
||||
protected _getFullscreenButton(
|
||||
config: FrigateCardConfig,
|
||||
inFullscreenMode?: boolean,
|
||||
fullscreenManager?: FullscreenManager | null,
|
||||
): MenuItem | null {
|
||||
return !isBeingCasted()
|
||||
const inFullscreen = fullscreenManager?.isInFullscreen();
|
||||
return fullscreenManager?.isSupported()
|
||||
? {
|
||||
icon: inFullscreenMode ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
||||
icon: inFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
||||
...config.menu.buttons.fullscreen,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
title: localize('config.menu.buttons.fullscreen'),
|
||||
tap_action: createGeneralAction('fullscreen') as FrigateCardCustomAction,
|
||||
style: inFullscreenMode ? this._getEmphasizedStyle() : {},
|
||||
style: inFullscreen ? this._getEmphasizedStyle() : {},
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -694,7 +696,7 @@ export class MenuButtonController {
|
||||
(frigateCardAction.frigate_card_action === 'default' &&
|
||||
options?.view?.is(config.view.default)) ||
|
||||
(frigateCardAction.frigate_card_action === 'fullscreen' &&
|
||||
!!options?.inFullscreenMode) ||
|
||||
!!options?.fullscreenManager?.isInFullscreen()) ||
|
||||
(frigateCardAction.frigate_card_action === 'camera_select' &&
|
||||
options?.view?.camera === frigateCardAction.camera)
|
||||
) {
|
||||
|
||||
@@ -19,7 +19,12 @@ import { CameraConfig, ImageMode, ImageViewConfig } from '../config/types.js';
|
||||
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import imageStyle from '../scss/image.scss';
|
||||
import { FrigateCardMediaPlayer, MediaLoadedInfo, Message } from '../types.js';
|
||||
import {
|
||||
FrigateCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
MediaLoadedInfo,
|
||||
Message,
|
||||
} from '../types.js';
|
||||
import { contentsChanged } from '../utils/basic.js';
|
||||
import { isHassDifferent } from '../utils/ha/index.js';
|
||||
import {
|
||||
@@ -123,6 +128,10 @@ export class FrigateCardImageBase extends LitElement implements FrigateCardMedia
|
||||
return this._cachedValueController?.value ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refImage.value ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the element should be updated.
|
||||
* @param changedProps The changed properties if any.
|
||||
|
||||
@@ -17,7 +17,7 @@ import { handleZoomSettingsObservedEvent } from '../components-lib/zoom/zoom-vie
|
||||
import { CameraConfig, ImageViewConfig } from '../config/types';
|
||||
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
||||
import basicBlockStyle from '../scss/basic-block.scss';
|
||||
import { FrigateCardMediaPlayer } from '../types.js';
|
||||
import { FrigateCardMediaPlayer, FullscreenElement } from '../types.js';
|
||||
import { aspectRatioToString } from '../utils/basic';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||
import './image-base';
|
||||
@@ -79,6 +79,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refImage.value?.getFullscreenElement() ?? null;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
||||
if (
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
||||
import {
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
} from '../../types.js';
|
||||
import { aspectRatioToString } from '../../utils/basic.js';
|
||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||
@@ -144,6 +148,10 @@ export class FrigateCardLiveProvider
|
||||
return (await this._refProvider.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refProvider.value?.getFullscreenElement() ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully resolved live provider.
|
||||
* @returns A live provider (that is not 'auto').
|
||||
|
||||
@@ -16,6 +16,7 @@ import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
||||
import {
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
Message,
|
||||
} from '../../../../types.js';
|
||||
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.js';
|
||||
@@ -102,6 +103,10 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
||||
return this._player?.video ? screenshotMedia(this._player.video) : null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._player?.video ?? null;
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this._player = undefined;
|
||||
this._message = null;
|
||||
@@ -151,7 +156,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
||||
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||
this._player.src = address;
|
||||
this._player.visibilityCheck = false;
|
||||
this._player.controls = this.controls;
|
||||
this._player.setControls(this.controls);
|
||||
|
||||
if (this.cameraConfig?.go2rtc?.modes && this.cameraConfig.go2rtc.modes.length) {
|
||||
this._player.mode = this.cameraConfig.go2rtc.modes.join(',');
|
||||
@@ -170,7 +175,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
||||
}
|
||||
|
||||
if (changedProps.has('controls') && this._player) {
|
||||
this._player.controls = this.controls;
|
||||
this._player.setControls(this.controls);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
+2
-1
@@ -29,8 +29,9 @@ export class VideoRTC extends HTMLElement {
|
||||
onmessage: Record<string, (msg: { type: string; value: string }) => void>;
|
||||
|
||||
// Custom methods/members.
|
||||
controls: boolean;
|
||||
containingPlayer: FrigateCardMediaPlayer | null;
|
||||
microphoneStream: MediaStream | null;
|
||||
reconnect();
|
||||
|
||||
setControls(controls: boolean): void;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ export class VideoRTC extends HTMLElement {
|
||||
this.containingPlayer = null;
|
||||
|
||||
/**
|
||||
* Whether to show or hide video controls.
|
||||
* Whether to show or hide video controls for videos created in future.
|
||||
* @type {boolean}}
|
||||
*/
|
||||
this.controls = true;
|
||||
@@ -191,6 +191,15 @@ export class VideoRTC extends HTMLElement {
|
||||
}
|
||||
}
|
||||
|
||||
setControls(controls) {
|
||||
this.controls = controls;
|
||||
|
||||
// If the video already exists the controls won't be changed by default.
|
||||
if (this.video) {
|
||||
setControlsOnVideo(this.video, controls);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set video source (WebSocket URL). Support relative path.
|
||||
* @param {string|URL} value
|
||||
|
||||
@@ -7,7 +7,7 @@ import '../../../patches/ha-camera-stream';
|
||||
import '../../../patches/ha-hls-player.js';
|
||||
import '../../../patches/ha-web-rtc-player.js';
|
||||
import liveHAStyle from '../../../scss/live-ha.scss';
|
||||
import { FrigateCardMediaPlayer } from '../../../types.js';
|
||||
import { FrigateCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
||||
|
||||
@customElement('frigate-card-live-ha')
|
||||
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
||||
@@ -58,6 +58,10 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
||||
return (await this._playerRef.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._playerRef.value?.getFullscreenElement() ?? null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import { CameraConfig } from '../../../config/types';
|
||||
import basicBlockStyle from '../../../scss/basic-block.scss';
|
||||
import { FrigateCardMediaPlayer } from '../../../types.js';
|
||||
import { FrigateCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
||||
import '../../image-base.js';
|
||||
|
||||
@customElement('frigate-card-live-image')
|
||||
@@ -53,6 +53,10 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._refImage.value?.getFullscreenElement() ?? null;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.cameraConfig) {
|
||||
return;
|
||||
|
||||
@@ -17,6 +17,7 @@ import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
||||
import {
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
Message,
|
||||
} from '../../../types.js';
|
||||
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.js';
|
||||
@@ -101,6 +102,10 @@ export class FrigateCardLiveJSMPEG extends LitElement implements FrigateCardMedi
|
||||
return this._jsmpegCanvasElement?.toDataURL('image/jpeg') ?? null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._jsmpegCanvasElement ?? null;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (
|
||||
['cameraConfig', 'cameraEndpoints'].some((prop) => changedProperties.has(prop))
|
||||
|
||||
@@ -15,7 +15,12 @@ import { getTechnologyForVideoRTC } from '../../../components-lib/live/utils/get
|
||||
import { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
||||
import { localize } from '../../../localize/localize.js';
|
||||
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
|
||||
import { FrigateCardError, FrigateCardMediaPlayer, Message } from '../../../types.js';
|
||||
import {
|
||||
FrigateCardError,
|
||||
FrigateCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
Message,
|
||||
} from '../../../types.js';
|
||||
import { mayHaveAudio } from '../../../utils/audio.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
@@ -64,54 +69,58 @@ export class FrigateCardLiveWebRTCCard
|
||||
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
||||
|
||||
public async play(): Promise<void> {
|
||||
return this._getPlayer()?.play();
|
||||
return this._getVideo()?.play();
|
||||
}
|
||||
|
||||
public async pause(): Promise<void> {
|
||||
this._getPlayer()?.pause();
|
||||
this._getVideo()?.pause();
|
||||
}
|
||||
|
||||
public async mute(): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
player.muted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async unmute(): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
player.muted = false;
|
||||
}
|
||||
}
|
||||
|
||||
public isMuted(): boolean {
|
||||
return this._getPlayer()?.muted ?? true;
|
||||
return this._getVideo()?.muted ?? true;
|
||||
}
|
||||
|
||||
public async seek(seconds: number): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
player.currentTime = seconds;
|
||||
}
|
||||
}
|
||||
|
||||
public async setControls(controls?: boolean): Promise<void> {
|
||||
const player = this._getPlayer();
|
||||
const player = this._getVideo();
|
||||
if (player) {
|
||||
setControlsOnVideo(player, controls ?? this.controls);
|
||||
}
|
||||
}
|
||||
|
||||
public isPaused(): boolean {
|
||||
return this._getPlayer()?.paused ?? true;
|
||||
return this._getVideo()?.paused ?? true;
|
||||
}
|
||||
|
||||
public async getScreenshotURL(): Promise<string | null> {
|
||||
const video = this._getPlayer();
|
||||
const video = this._getVideo();
|
||||
return video ? screenshotMedia(video) : null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._getVideo();
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
@@ -141,7 +150,7 @@ export class FrigateCardLiveWebRTCCard
|
||||
* Get the underlying video player.
|
||||
* @returns The player or `null` if not found.
|
||||
*/
|
||||
protected _getPlayer(): HTMLVideoElement | null {
|
||||
protected _getVideo(): HTMLVideoElement | null {
|
||||
return this._getVideoRTC()?.video ?? null;
|
||||
}
|
||||
|
||||
@@ -236,7 +245,7 @@ export class FrigateCardLiveWebRTCCard
|
||||
// media load event.
|
||||
this.updateComplete.then(() => {
|
||||
const videoRTC = this._getVideoRTC();
|
||||
const video = this._getPlayer();
|
||||
const video = this._getVideo();
|
||||
if (video) {
|
||||
setControlsOnVideo(video, this.controls);
|
||||
video.onloadeddata = () => {
|
||||
|
||||
@@ -19,6 +19,7 @@ import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
||||
import {
|
||||
ExtendedHomeAssistant,
|
||||
FrigateCardMediaPlayer,
|
||||
FullscreenElement,
|
||||
ResolvedMedia,
|
||||
} from '../../types.js';
|
||||
import { mayHaveAudio } from '../../utils/audio.js';
|
||||
@@ -169,6 +170,15 @@ export class FrigateCardViewerProvider
|
||||
return null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
if (this._refFrigateCardMediaPlayer.value) {
|
||||
return this._refFrigateCardMediaPlayer.value.getFullscreenElement();
|
||||
} else if (this._refVideoProvider.value) {
|
||||
return this._refVideoProvider.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async _switchToRelatedClipView(): Promise<void> {
|
||||
const view = this.viewManagerEpoch?.manager.getView();
|
||||
if (
|
||||
|
||||
@@ -12,14 +12,14 @@
|
||||
import { css, CSSResultGroup, html, nothing, PropertyValues, unsafeCSS } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
import { query } from 'lit/decorators/query.js';
|
||||
import { FrigateCardMediaPlayer, MediaLoadedInfo } from '../types.js';
|
||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||
import { FrigateCardMediaPlayer, FullscreenElement, MediaLoadedInfo } from '../types.js';
|
||||
import {
|
||||
createMediaLoadedInfo,
|
||||
dispatchExistingMediaLoadedInfoAsEvent,
|
||||
} 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(() => {
|
||||
// ========================================================================================
|
||||
@@ -93,6 +93,10 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
||||
return this._player ? await this._player.getScreenshotURL() : null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._player?.getFullscreenElement() ?? null;
|
||||
}
|
||||
|
||||
protected _storeMediaLoadedInfoHandler(
|
||||
stream: StreamType,
|
||||
ev: CustomEvent<MediaLoadedInfo>,
|
||||
|
||||
@@ -15,7 +15,7 @@ import { query } from 'lit/decorators/query.js';
|
||||
import { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { renderMessage } from '../components/message.js';
|
||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||
import { FrigateCardMediaPlayer } from '../types.js';
|
||||
import { FrigateCardMediaPlayer, FullscreenElement } from '../types.js';
|
||||
import { mayHaveAudio } from '../utils/audio.js';
|
||||
import { errorToConsole } from '../utils/basic.js';
|
||||
import {
|
||||
@@ -91,6 +91,10 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
||||
return this._video ? screenshotMedia(this._video) : null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._video;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// Minor modifications from:
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ifDefined } from 'lit/directives/if-defined.js';
|
||||
import { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||
import { renderMessage } from '../components/message.js';
|
||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
||||
import { FrigateCardMediaPlayer } from '../types.js';
|
||||
import { FrigateCardMediaPlayer, FullscreenElement } from '../types.js';
|
||||
import { mayHaveAudio } from '../utils/audio.js';
|
||||
import {
|
||||
dispatchMediaLoadedEvent,
|
||||
@@ -90,6 +90,10 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
||||
return this._video ? screenshotMedia(this._video) : null;
|
||||
}
|
||||
|
||||
public getFullscreenElement(): FullscreenElement | null {
|
||||
return this._video ?? null;
|
||||
}
|
||||
|
||||
// =====================================================================================
|
||||
// Minor modifications from:
|
||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-web-rtc-player.ts
|
||||
|
||||
@@ -54,6 +54,15 @@ export interface Message {
|
||||
troubleshootingURL?: string;
|
||||
}
|
||||
|
||||
export type WebkitHTMLVideoElement = HTMLVideoElement & {
|
||||
webkitDisplayingFullscreen: boolean;
|
||||
webkitSupportsFullscreen: boolean;
|
||||
webkitEnterFullscreen: () => void;
|
||||
webkitExitFullscreen: () => void;
|
||||
};
|
||||
|
||||
export type FullscreenElement = HTMLElement;
|
||||
|
||||
export interface FrigateCardMediaPlayer {
|
||||
play(): Promise<void>;
|
||||
pause(): Promise<void>;
|
||||
@@ -65,6 +74,7 @@ export interface FrigateCardMediaPlayer {
|
||||
// If no value for controls if specified, the player should use the default.
|
||||
setControls(controls?: boolean): Promise<void>;
|
||||
isPaused(): boolean;
|
||||
getFullscreenElement(): FullscreenElement | null;
|
||||
}
|
||||
|
||||
export type LovelaceCardWithEditor = LovelaceCard & {
|
||||
|
||||
@@ -36,7 +36,7 @@ export const getReleaseVersion = (): string => {
|
||||
return releaseVersion;
|
||||
};
|
||||
|
||||
export interface Diagnostics {
|
||||
interface Diagnostics {
|
||||
card_version: string;
|
||||
browser: string;
|
||||
date: Date;
|
||||
|
||||
@@ -6,11 +6,13 @@ import {
|
||||
evaluateConditionViaEvent,
|
||||
getOverriddenConfig,
|
||||
} from '../../src/card-controller/conditions-manager';
|
||||
import { MicrophoneState } from '../../src/card-controller/types';
|
||||
import { FrigateCardCondition } from '../../src/config/types';
|
||||
import {
|
||||
createCardAPI,
|
||||
createCondition,
|
||||
createConfig,
|
||||
createMediaLoadedInfo,
|
||||
createStateEntity,
|
||||
createUser,
|
||||
} from '../test-utils';
|
||||
@@ -780,9 +782,9 @@ describe('ConditionsManager', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'media_loaded' as const, media_loaded: true }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ media_loaded: true });
|
||||
manager.setState({ mediaLoadedInfo: createMediaLoadedInfo() });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({ media_loaded: false });
|
||||
manager.setState({ mediaLoadedInfo: null });
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -898,17 +900,27 @@ describe('ConditionsManager', () => {
|
||||
});
|
||||
|
||||
describe('with microphone condition', () => {
|
||||
const createMicrophoneState = (
|
||||
state: Partial<MicrophoneState>,
|
||||
): MicrophoneState => {
|
||||
return {
|
||||
connected: false,
|
||||
muted: false,
|
||||
forbidden: false,
|
||||
...state,
|
||||
};
|
||||
};
|
||||
it('empty', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'microphone' as const }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({ microphone: { connected: true } });
|
||||
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({ microphone: { connected: false } });
|
||||
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({ microphone: { muted: true } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({ microphone: { muted: false } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -916,9 +928,9 @@ describe('ConditionsManager', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'microphone' as const, connected: true }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { connected: true } });
|
||||
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({ microphone: { connected: false } });
|
||||
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -926,9 +938,9 @@ describe('ConditionsManager', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'microphone' as const, connected: false }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { connected: true } });
|
||||
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { connected: false } });
|
||||
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -936,9 +948,9 @@ describe('ConditionsManager', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'microphone' as const, muted: true }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { muted: true } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({ microphone: { muted: false } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
|
||||
@@ -946,9 +958,9 @@ describe('ConditionsManager', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
const conditions = [{ condition: 'microphone' as const, muted: false }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { muted: true } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { muted: false } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -958,13 +970,17 @@ describe('ConditionsManager', () => {
|
||||
{ condition: 'microphone' as const, muted: false, connected: true },
|
||||
];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { muted: true } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { muted: false } });
|
||||
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { connected: false, muted: false } });
|
||||
manager.setState({
|
||||
microphone: createMicrophoneState({ connected: false, muted: false }),
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({ microphone: { connected: true, muted: false } });
|
||||
manager.setState({
|
||||
microphone: createMicrophoneState({ connected: true, muted: false }),
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1033,11 +1049,11 @@ describe('ConditionsManager', () => {
|
||||
const conditions = [{ condition: 'user_agent' as const, user_agent: userAgent }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: userAgent,
|
||||
userAgent: userAgent,
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: 'Something else',
|
||||
userAgent: 'Something else',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
@@ -1049,11 +1065,11 @@ describe('ConditionsManager', () => {
|
||||
];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: userAgent,
|
||||
userAgent: userAgent,
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: 'Something else',
|
||||
userAgent: 'Something else',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
@@ -1063,11 +1079,11 @@ describe('ConditionsManager', () => {
|
||||
const conditions = [{ condition: 'user_agent' as const, companion: true }];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: 'Home Assistant/',
|
||||
userAgent: 'Home Assistant/',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: userAgent,
|
||||
userAgent: userAgent,
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
@@ -1084,14 +1100,37 @@ describe('ConditionsManager', () => {
|
||||
];
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
manager.setState({
|
||||
user_agent: 'Home Assistant/',
|
||||
userAgent: 'Home Assistant/',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||
manager.setState({
|
||||
user_agent: 'Something else',
|
||||
userAgent: 'Something else',
|
||||
});
|
||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add listener', () => {
|
||||
const listener = vi.fn();
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.addListener(listener);
|
||||
|
||||
const state = { fullscreen: true };
|
||||
manager.setState(state);
|
||||
|
||||
expect(listener).toBeCalledWith(state, {});
|
||||
});
|
||||
|
||||
it('should remove listener', () => {
|
||||
const listener = vi.fn();
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.addListener(listener);
|
||||
manager.removeListener(listener);
|
||||
|
||||
const state = { fullscreen: true };
|
||||
manager.setState(state);
|
||||
|
||||
expect(listener).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { CardController } from '../../src/card-controller/controller';
|
||||
import { DefaultManager } from '../../src/card-controller/default-manager';
|
||||
import { DownloadManager } from '../../src/card-controller/download-manager';
|
||||
import { ExpandManager } from '../../src/card-controller/expand-manager';
|
||||
import { FullscreenManager } from '../../src/card-controller/fullscreen-manager';
|
||||
import { FullscreenManager } from '../../src/card-controller/fullscreen/fullscreen-manager';
|
||||
import { HASSManager } from '../../src/card-controller/hass/hass-manager';
|
||||
import { InitializationManager } from '../../src/card-controller/initialization-manager';
|
||||
import { InteractionManager } from '../../src/card-controller/interaction-manager';
|
||||
@@ -42,7 +42,7 @@ vi.mock('../../src/card-controller/config/config-manager');
|
||||
vi.mock('../../src/card-controller/default-manager');
|
||||
vi.mock('../../src/card-controller/download-manager');
|
||||
vi.mock('../../src/card-controller/expand-manager');
|
||||
vi.mock('../../src/card-controller/fullscreen-manager');
|
||||
vi.mock('../../src/card-controller/fullscreen/fullscreen-manager');
|
||||
vi.mock('../../src/card-controller/hass/hass-manager');
|
||||
vi.mock('../../src/card-controller/initialization-manager');
|
||||
vi.mock('../../src/card-controller/interaction-manager');
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('ExpandManager', () => {
|
||||
manager.setExpanded(true);
|
||||
|
||||
expect(manager.isExpanded()).toBeTruthy();
|
||||
expect(api.getFullscreenManager().stopFullscreen).toBeCalled();
|
||||
expect(api.getFullscreenManager().setFullscreen).toBeCalledWith(false);
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true });
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
});
|
||||
@@ -37,7 +37,7 @@ describe('ExpandManager', () => {
|
||||
|
||||
manager.setExpanded(true);
|
||||
|
||||
expect(api.getFullscreenManager().stopFullscreen).not.toBeCalled();
|
||||
expect(api.getFullscreenManager().setFullscreen).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should toggle expanded', () => {
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FullscreenManager } from '../../src/card-controller/fullscreen-manager';
|
||||
import { createCardAPI } from '../test-utils';
|
||||
|
||||
vi.mock('screenfull', () => ({
|
||||
default: {
|
||||
exit: vi.fn(),
|
||||
toggle: vi.fn(),
|
||||
off: vi.fn(),
|
||||
on: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const setScreenfulEnabled = (enabled: boolean): void => {
|
||||
Object.defineProperty(screenfull, 'isEnabled', { value: enabled, writable: true });
|
||||
};
|
||||
|
||||
const setScreenfulFullscreen = (fullscreen: boolean): void => {
|
||||
Object.defineProperty(screenfull, 'isFullscreen', {
|
||||
value: fullscreen,
|
||||
writable: true,
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('FullscreenManager', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new FullscreenManager(api);
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
setScreenfulFullscreen(false);
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ fullscreen: false });
|
||||
});
|
||||
|
||||
it('should correctly determine whether in fullscreen', () => {
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
setScreenfulFullscreen(true);
|
||||
expect(manager.isInFullscreen()).toBeTruthy();
|
||||
|
||||
setScreenfulFullscreen(false);
|
||||
expect(manager.isInFullscreen()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should toggle fullscreen', () => {
|
||||
const toggle = vi.mocked(screenfull.toggle);
|
||||
const element = document.createElement('div');
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
const manager = new FullscreenManager(api);
|
||||
|
||||
manager.toggleFullscreen();
|
||||
|
||||
expect(toggle).toBeCalledWith(element);
|
||||
});
|
||||
|
||||
it('should stop fullscreen', () => {
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
const exit = vi.mocked(screenfull.exit);
|
||||
|
||||
manager.stopFullscreen();
|
||||
|
||||
expect(exit).toBeCalled();
|
||||
});
|
||||
|
||||
it('should disconnect', () => {
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
const off = vi.mocked(screenfull.off);
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
manager.disconnect();
|
||||
|
||||
expect(off).toBeCalledWith('change', expect.anything());
|
||||
});
|
||||
|
||||
it('should not disconnect when screenfull disabled', () => {
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
const off = vi.mocked(screenfull.off);
|
||||
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
manager.disconnect();
|
||||
|
||||
expect(off).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should connect', () => {
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
const on = vi.mocked(screenfull.on);
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
manager.connect();
|
||||
|
||||
expect(on).toBeCalledWith('change', expect.anything());
|
||||
});
|
||||
|
||||
it('should not connect when screenfull disabled', () => {
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
const on = vi.mocked(screenfull.on);
|
||||
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
manager.connect();
|
||||
|
||||
expect(on).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should make correct api calls on fullscreen change', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new FullscreenManager(api);
|
||||
const on = vi.mocked(screenfull.on);
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
setScreenfulFullscreen(true);
|
||||
|
||||
manager.connect();
|
||||
|
||||
expect(on).toBeCalled();
|
||||
on.mock.calls[0][1](new Event('fullscreen'));
|
||||
|
||||
expect(api.getExpandManager().setExpanded).toBeCalledWith(false);
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
||||
fullscreen: true,
|
||||
});
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { FullscreenProviderFactory } from '../../../src/card-controller/fullscreen/factory';
|
||||
import { ScreenfullFullScreenProvider } from '../../../src/card-controller/fullscreen/screenfull';
|
||||
import { WebkitFullScreenProvider } from '../../../src/card-controller/fullscreen/webkit';
|
||||
import { WebkitHTMLVideoElement } from '../../../src/types';
|
||||
import { createCardAPI, setScreenfulEnabled } from '../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('FullscreenProviderFactory', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should return screenful when enabled', () => {
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
expect(FullscreenProviderFactory.create(createCardAPI(), vi.fn())).toBeInstanceOf(
|
||||
ScreenfullFullScreenProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return webkit when enabled', () => {
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
const element = document.createElement('video') as HTMLVideoElement &
|
||||
Partial<WebkitHTMLVideoElement>;
|
||||
element['webkitEnterFullscreen'] = vi.fn();
|
||||
|
||||
const stubDocument = mock<Document>();
|
||||
stubDocument.createElement.mockReturnValue(element);
|
||||
|
||||
vi.stubGlobal('document', stubDocument);
|
||||
|
||||
expect(FullscreenProviderFactory.create(createCardAPI(), vi.fn())).toBeInstanceOf(
|
||||
WebkitFullScreenProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it('should return null without any provider', () => {
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
const element = document.createElement('video') as HTMLVideoElement &
|
||||
Partial<WebkitHTMLVideoElement>;
|
||||
element['webkitEnterFullscreen'] = undefined;
|
||||
|
||||
const stubDocument = mock<Document>();
|
||||
stubDocument.createElement.mockReturnValue(element);
|
||||
|
||||
vi.stubGlobal('document', stubDocument);
|
||||
|
||||
expect(FullscreenProviderFactory.create(createCardAPI(), vi.fn())).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { FullscreenProviderFactory } from '../../../src/card-controller/fullscreen/factory';
|
||||
import { FullscreenManager } from '../../../src/card-controller/fullscreen/fullscreen-manager';
|
||||
import { FullscreenProvider } from '../../../src/card-controller/fullscreen/types';
|
||||
import { createCardAPI } from '../../test-utils';
|
||||
|
||||
vi.mock('../../../src/card-controller/fullscreen/factory');
|
||||
|
||||
describe('FullscreenManager', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new FullscreenManager(api, mock<FullscreenProvider>());
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ fullscreen: false });
|
||||
});
|
||||
|
||||
it('should correctly determine whether in fullscreen', () => {
|
||||
const provider = mock<FullscreenProvider>();
|
||||
const manager = new FullscreenManager(createCardAPI(), provider);
|
||||
|
||||
provider.isInFullscreen.mockReturnValue(true);
|
||||
|
||||
expect(manager.isInFullscreen()).toBeTruthy();
|
||||
|
||||
provider.isInFullscreen.mockReturnValue(false);
|
||||
|
||||
expect(manager.isInFullscreen()).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('describe toggle fullscreen', () => {
|
||||
it.each([
|
||||
[true, false],
|
||||
[false, true],
|
||||
])('%s -> %s', async (state: boolean, expected: boolean) => {
|
||||
const provider = mock<FullscreenProvider>();
|
||||
provider.isInFullscreen.mockReturnValue(state);
|
||||
|
||||
const manager = new FullscreenManager(createCardAPI(), provider);
|
||||
|
||||
manager.toggleFullscreen();
|
||||
|
||||
expect(provider.setFullscreen).toBeCalledWith(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('describe set fullscreen', () => {
|
||||
it.each([[true], [false]])('%s', async (fullscreen: boolean) => {
|
||||
const provider = mock<FullscreenProvider>();
|
||||
const manager = new FullscreenManager(createCardAPI(), provider);
|
||||
|
||||
manager.setFullscreen(fullscreen);
|
||||
|
||||
expect(provider.setFullscreen).toBeCalledWith(fullscreen);
|
||||
});
|
||||
});
|
||||
|
||||
it('should connect', () => {
|
||||
const provider = mock<FullscreenProvider>();
|
||||
const manager = new FullscreenManager(createCardAPI(), provider);
|
||||
|
||||
manager.connect();
|
||||
|
||||
expect(provider.connect).toBeCalled();
|
||||
});
|
||||
|
||||
it('should disconnect', () => {
|
||||
const provider = mock<FullscreenProvider>();
|
||||
const manager = new FullscreenManager(createCardAPI(), provider);
|
||||
|
||||
manager.disconnect();
|
||||
|
||||
expect(provider.disconnect).toBeCalled();
|
||||
});
|
||||
|
||||
describe('should confirm whether fullscreen is supported', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
||||
});
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('when being casted', () => {
|
||||
vi.stubGlobal('navigator', { userAgent: 'CrKey/' });
|
||||
|
||||
const api = createCardAPI();
|
||||
const provider = mock<FullscreenProvider>();
|
||||
provider.isSupported.mockReturnValue(true);
|
||||
|
||||
const manager = new FullscreenManager(api, provider);
|
||||
|
||||
expect(manager.isSupported()).toBeFalsy();
|
||||
});
|
||||
|
||||
describe('should return provider support', () => {
|
||||
it.each([[true], [false]])('%s', async (support: boolean) => {
|
||||
const api = createCardAPI();
|
||||
const provider = mock<FullscreenProvider>();
|
||||
provider.isSupported.mockReturnValue(support);
|
||||
const manager = new FullscreenManager(api, provider);
|
||||
|
||||
expect(manager.isSupported()).toBe(support);
|
||||
});
|
||||
});
|
||||
|
||||
it('without a provider', () => {
|
||||
vi.mocked(FullscreenProviderFactory).create.mockReturnValue(null);
|
||||
|
||||
const manager = new FullscreenManager(createCardAPI());
|
||||
|
||||
expect(manager.isSupported()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle state change', () => {
|
||||
it.each([[true], [false]])('%s', async (fullscreen: boolean) => {
|
||||
const api = createCardAPI();
|
||||
|
||||
const provider = mock<FullscreenProvider>();
|
||||
vi.mocked(FullscreenProviderFactory.create).mockReturnValue(provider);
|
||||
|
||||
new FullscreenManager(api);
|
||||
|
||||
const handler = vi.mocked(FullscreenProviderFactory.create).mock.calls[0][1];
|
||||
|
||||
provider.isInFullscreen.mockReturnValue(fullscreen);
|
||||
|
||||
handler();
|
||||
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
||||
fullscreen: fullscreen,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create provider from factory', () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
new FullscreenManager(api);
|
||||
|
||||
expect(FullscreenProviderFactory.create).toBeCalledWith(api, expect.anything());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,162 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ScreenfullFullScreenProvider } from '../../../../src/card-controller/fullscreen/screenfull';
|
||||
import { createCardAPI, setScreenfulEnabled } from '../../../test-utils';
|
||||
|
||||
vi.mock('screenfull', () => ({
|
||||
default: {
|
||||
exit: vi.fn(),
|
||||
request: vi.fn(),
|
||||
off: vi.fn(),
|
||||
on: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const setScreenfulFullscreen = (fullscreen: boolean): void => {
|
||||
Object.defineProperty(screenfull, 'isFullscreen', {
|
||||
value: fullscreen,
|
||||
writable: true,
|
||||
});
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('ScreenfullFullScreenProvider', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('should connect', () => {
|
||||
it('should connect if enabled', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
const on = vi.mocked(screenfull.on);
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
provider.connect();
|
||||
|
||||
expect(on).toBeCalledWith('change', expect.anything());
|
||||
});
|
||||
|
||||
it('should not connect if not enabled', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
const on = vi.mocked(screenfull.on);
|
||||
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
provider.connect();
|
||||
|
||||
expect(on).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should disconnect', () => {
|
||||
it('should disconnect if enabled', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
const off = vi.mocked(screenfull.off);
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
provider.disconnect();
|
||||
|
||||
expect(off).toBeCalledWith('change', expect.anything());
|
||||
});
|
||||
|
||||
it('should not disconnect if not enabled', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
const off = vi.mocked(screenfull.off);
|
||||
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
provider.disconnect();
|
||||
|
||||
expect(off).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return if in fullscreen', () => {
|
||||
it('should return true if in fullscreen', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
setScreenfulFullscreen(true);
|
||||
|
||||
expect(provider.isInFullscreen()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false if not in fullscreen', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
setScreenfulFullscreen(false);
|
||||
|
||||
expect(provider.isInFullscreen()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return false if not supported', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
|
||||
setScreenfulEnabled(false);
|
||||
setScreenfulFullscreen(true);
|
||||
|
||||
expect(provider.isInFullscreen()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return if supported', () => {
|
||||
it('should return true if supported', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
expect(provider.isSupported()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false if not supported', () => {
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
expect(provider.isSupported()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set fullscreen', () => {
|
||||
it('should request fullscreen if fullscreen is true', () => {
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
const provider = new ScreenfullFullScreenProvider(api, vi.fn());
|
||||
|
||||
provider.setFullscreen(true);
|
||||
|
||||
expect(screenfull.request).toBeCalledWith(element);
|
||||
});
|
||||
|
||||
it('should exit fullscreen if fullscreen is false', () => {
|
||||
setScreenfulEnabled(true);
|
||||
|
||||
const provider = new ScreenfullFullScreenProvider(createCardAPI(), vi.fn());
|
||||
|
||||
provider.setFullscreen(false);
|
||||
|
||||
expect(screenfull.exit).toBeCalled();
|
||||
});
|
||||
|
||||
it('should take no action if not supported', () => {
|
||||
setScreenfulEnabled(false);
|
||||
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
const provider = new ScreenfullFullScreenProvider(api, vi.fn());
|
||||
|
||||
provider.setFullscreen(true);
|
||||
provider.setFullscreen(false);
|
||||
|
||||
expect(screenfull.request).not.toBeCalled();
|
||||
expect(screenfull.exit).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,236 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CardController } from '../../../../src/card-controller/controller';
|
||||
import { WebkitFullScreenProvider } from '../../../../src/card-controller/fullscreen/webkit';
|
||||
import { FrigateCardMediaPlayer, WebkitHTMLVideoElement } from '../../../../src/types';
|
||||
import { createCardAPI, createMediaLoadedInfo } from '../../../test-utils';
|
||||
|
||||
const createWebkitVideoElement = (): HTMLVideoElement &
|
||||
Partial<WebkitHTMLVideoElement> => {
|
||||
return document.createElement('video');
|
||||
};
|
||||
|
||||
const setElement = (api: CardController, element: HTMLElement): void => {
|
||||
const player = mock<FrigateCardMediaPlayer>();
|
||||
player.getFullscreenElement.mockReturnValue(element);
|
||||
|
||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||
createMediaLoadedInfo({
|
||||
player: player,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('WebkitFullScreenProvider', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should connect', () => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
provider.connect();
|
||||
|
||||
expect(api.getConditionsManager().addListener).toBeCalledWith(expect.anything());
|
||||
});
|
||||
|
||||
it('should disconnect', () => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
provider.disconnect();
|
||||
|
||||
expect(api.getConditionsManager().removeListener).toBeCalledWith(expect.anything());
|
||||
});
|
||||
|
||||
describe('should return if in fullscreen', () => {
|
||||
it.each([[true], [false]])('%s', async (fullscreen: boolean) => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.webkitDisplayingFullscreen = fullscreen;
|
||||
|
||||
setElement(api, element);
|
||||
|
||||
expect(provider.isInFullscreen()).toBe(fullscreen);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should return if supported', () => {
|
||||
it.each([[true], [false]])('%s', async (supported: boolean) => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.webkitSupportsFullscreen = supported;
|
||||
|
||||
setElement(api, element);
|
||||
|
||||
expect(provider.isSupported()).toBe(supported);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should set fullscreen', () => {
|
||||
it('should request fullscreen if fullscreen is true', () => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.webkitEnterFullscreen = vi.fn();
|
||||
element.webkitSupportsFullscreen = true;
|
||||
|
||||
setElement(api, element);
|
||||
|
||||
provider.setFullscreen(true);
|
||||
|
||||
expect(element.webkitEnterFullscreen).toBeCalled();
|
||||
});
|
||||
|
||||
it('should exit fullscreen if fullscreen is true', () => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.webkitExitFullscreen = vi.fn();
|
||||
element.webkitSupportsFullscreen = true;
|
||||
|
||||
setElement(api, element);
|
||||
|
||||
provider.setFullscreen(false);
|
||||
|
||||
expect(element.webkitExitFullscreen).toBeCalled();
|
||||
});
|
||||
|
||||
it('should take no action if not supported', () => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.webkitEnterFullscreen = vi.fn();
|
||||
element.webkitExitFullscreen = vi.fn();
|
||||
element.webkitSupportsFullscreen = false;
|
||||
|
||||
setElement(api, element);
|
||||
|
||||
provider.setFullscreen(true);
|
||||
provider.setFullscreen(false);
|
||||
|
||||
expect(element.webkitEnterFullscreen).not.toBeCalled();
|
||||
expect(element.webkitExitFullscreen).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should take no action if element is not a video', () => {
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, vi.fn());
|
||||
|
||||
const element = document.createElement('img') as HTMLImageElement &
|
||||
Partial<WebkitHTMLVideoElement>;
|
||||
element.webkitEnterFullscreen = vi.fn();
|
||||
element.webkitExitFullscreen = vi.fn();
|
||||
|
||||
setElement(api, element);
|
||||
|
||||
provider.setFullscreen(true);
|
||||
provider.setFullscreen(false);
|
||||
|
||||
expect(element.webkitEnterFullscreen).not.toBeCalled();
|
||||
expect(element.webkitExitFullscreen).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle state changes', () => {
|
||||
describe('should call handler when fullscreen state changes', () => {
|
||||
it.each([['webkitbeginfullscreen'], ['webkitendfullscreen']])(
|
||||
'%s',
|
||||
(event: string) => {
|
||||
const handler = vi.fn();
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, handler);
|
||||
|
||||
provider.connect();
|
||||
|
||||
const conditionChangeHandler = vi.mocked(
|
||||
api.getConditionsManager().addListener,
|
||||
).mock.calls[0][0];
|
||||
|
||||
const element_1 = createWebkitVideoElement();
|
||||
const player_1 = mock<FrigateCardMediaPlayer>();
|
||||
player_1.getFullscreenElement.mockReturnValue(element_1);
|
||||
const mediaLoadedInfo_1 = createMediaLoadedInfo({ player: player_1 });
|
||||
|
||||
conditionChangeHandler({ mediaLoadedInfo: mediaLoadedInfo_1 });
|
||||
|
||||
element_1.dispatchEvent(new Event(event));
|
||||
|
||||
expect(handler).toBeCalledTimes(1);
|
||||
|
||||
const element_2 = createWebkitVideoElement();
|
||||
const player_2 = mock<FrigateCardMediaPlayer>();
|
||||
player_2.getFullscreenElement.mockReturnValue(element_2);
|
||||
const mediaLoadedInfo_2 = createMediaLoadedInfo({ player: player_2 });
|
||||
|
||||
conditionChangeHandler(
|
||||
{ mediaLoadedInfo: mediaLoadedInfo_2 },
|
||||
{ mediaLoadedInfo: mediaLoadedInfo_1 },
|
||||
);
|
||||
|
||||
element_2.dispatchEvent(new Event(event));
|
||||
|
||||
expect(handler).toBeCalledTimes(2);
|
||||
|
||||
// Events on the old element should be ignored.
|
||||
element_1.dispatchEvent(new Event(event));
|
||||
|
||||
expect(handler).toBeCalledTimes(2);
|
||||
|
||||
// Test the media loaded info not changing.
|
||||
conditionChangeHandler(
|
||||
{ mediaLoadedInfo: mediaLoadedInfo_2 },
|
||||
{ mediaLoadedInfo: mediaLoadedInfo_2 },
|
||||
);
|
||||
|
||||
// Events on the new element should still be handled.
|
||||
element_2.dispatchEvent(new Event(event));
|
||||
|
||||
expect(handler).toBeCalledTimes(3);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should play the video after fullscreen ends', () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
const handler = vi.fn();
|
||||
const api = createCardAPI();
|
||||
const provider = new WebkitFullScreenProvider(api, handler);
|
||||
|
||||
provider.connect();
|
||||
|
||||
const conditionChangeHandler = vi.mocked(api.getConditionsManager().addListener).mock
|
||||
.calls[0][0];
|
||||
|
||||
const element = createWebkitVideoElement();
|
||||
element.play = vi.fn();
|
||||
|
||||
setElement(api, element);
|
||||
|
||||
const player = mock<FrigateCardMediaPlayer>();
|
||||
player.getFullscreenElement.mockReturnValue(element);
|
||||
const mediaLoadedInfo = createMediaLoadedInfo({ player });
|
||||
|
||||
conditionChangeHandler({ mediaLoadedInfo });
|
||||
|
||||
element.dispatchEvent(new Event('webkitendfullscreen'));
|
||||
|
||||
expect(element.play).not.toBeCalled();
|
||||
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(element.play).toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,9 @@ describe('MediaLoadedInfoManager', () => {
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
|
||||
manager.initialize();
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({ media_loaded: false });
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
||||
mediaLoadedInfo: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should set', () => {
|
||||
@@ -21,7 +23,7 @@ describe('MediaLoadedInfoManager', () => {
|
||||
expect(manager.has()).toBeTruthy();
|
||||
expect(manager.get()).toBe(mediaInfo);
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
||||
expect.objectContaining({ media_loaded: true }),
|
||||
expect.objectContaining({ mediaLoadedInfo: mediaInfo }),
|
||||
);
|
||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
||||
expect(api.getCardElementManager().update).toBeCalled();
|
||||
@@ -30,9 +32,9 @@ describe('MediaLoadedInfoManager', () => {
|
||||
it('should not set invalid media info', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const mediaInfo = createMediaLoadedInfo({ width: 0, height: 0 });
|
||||
const mediaLoadedInfo = createMediaLoadedInfo({ width: 0, height: 0 });
|
||||
|
||||
manager.set(mediaInfo);
|
||||
manager.set(mediaLoadedInfo);
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(manager.get()).toBeNull();
|
||||
@@ -42,18 +44,18 @@ describe('MediaLoadedInfoManager', () => {
|
||||
it('should get last known', () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new MediaLoadedInfoManager(api);
|
||||
const mediaInfo = createMediaLoadedInfo();
|
||||
const mediaLoadedInfo = createMediaLoadedInfo();
|
||||
|
||||
manager.set(mediaInfo);
|
||||
manager.set(mediaLoadedInfo);
|
||||
|
||||
expect(manager.has()).toBeTruthy();
|
||||
|
||||
manager.clear();
|
||||
|
||||
expect(manager.has()).toBeFalsy();
|
||||
expect(manager.getLastKnown()).toBe(mediaInfo);
|
||||
expect(manager.getLastKnown()).toBe(mediaLoadedInfo);
|
||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
||||
expect.objectContaining({ media_loaded: false }),
|
||||
expect.objectContaining({ mediaLoadedInfo }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||
import { CameraManager } from '../../src/camera-manager/manager';
|
||||
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types';
|
||||
import { FullscreenManager } from '../../src/card-controller/fullscreen/fullscreen-manager';
|
||||
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
||||
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
||||
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
||||
@@ -1121,10 +1122,19 @@ describe('MenuButtonController', () => {
|
||||
});
|
||||
|
||||
describe('should have fullscreen button', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('when not in fullscreen mode', () => {
|
||||
// Need to write a readonly property.
|
||||
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
||||
const buttons = calculateButtons(controller, { inFullscreenMode: false });
|
||||
|
||||
const fullscreenManager = mock<FullscreenManager>();
|
||||
vi.mocked(fullscreenManager.isInFullscreen).mockReturnValue(false);
|
||||
vi.mocked(fullscreenManager.isSupported).mockReturnValue(true);
|
||||
|
||||
const buttons = calculateButtons(controller, { fullscreenManager });
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:fullscreen',
|
||||
@@ -1139,7 +1149,11 @@ describe('MenuButtonController', () => {
|
||||
|
||||
it('when in fullscreen mode', () => {
|
||||
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
||||
const buttons = calculateButtons(controller, { inFullscreenMode: true });
|
||||
const fullscreenManager = mock<FullscreenManager>();
|
||||
vi.mocked(fullscreenManager.isInFullscreen).mockReturnValue(true);
|
||||
vi.mocked(fullscreenManager.isSupported).mockReturnValue(true);
|
||||
|
||||
const buttons = calculateButtons(controller, { fullscreenManager });
|
||||
|
||||
expect(buttons).toContainEqual({
|
||||
icon: 'mdi:fullscreen-exit',
|
||||
@@ -1151,6 +1165,19 @@ describe('MenuButtonController', () => {
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
});
|
||||
|
||||
it('when not supported', () => {
|
||||
// Need to write a readonly property.
|
||||
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
||||
const fullscreenManager = mock<FullscreenManager>();
|
||||
vi.mocked(fullscreenManager.isSupported).mockReturnValue(false);
|
||||
|
||||
const buttons = calculateButtons(controller, { fullscreenManager });
|
||||
|
||||
expect(buttons).not.toContainEqual(
|
||||
expect.objectContaining({ title: 'Fullscreen' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should have expand button', () => {
|
||||
@@ -1767,8 +1794,11 @@ describe('MenuButtonController', () => {
|
||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
|
||||
};
|
||||
controller.addDynamicMenuButton(button);
|
||||
const fullscreenManager = mock<FullscreenManager>();
|
||||
vi.mocked(fullscreenManager.isInFullscreen).mockReturnValue(true);
|
||||
vi.mocked(fullscreenManager.isSupported).mockReturnValue(true);
|
||||
|
||||
expect(calculateButtons(controller, { inFullscreenMode: true })).toContainEqual({
|
||||
expect(calculateButtons(controller, { fullscreenManager })).toContainEqual({
|
||||
...button,
|
||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||
});
|
||||
|
||||
+6
-1
@@ -1,6 +1,7 @@
|
||||
import { CurrentUser, HASSDomEvent } from '@dermotduffy/custom-card-helpers';
|
||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||
import { LitElement } from 'lit';
|
||||
import screenfull from 'screenfull';
|
||||
import { expect, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { Camera } from '../src/camera-manager/camera';
|
||||
@@ -24,7 +25,7 @@ import { CardController } from '../src/card-controller/controller';
|
||||
import { DefaultManager } from '../src/card-controller/default-manager';
|
||||
import { DownloadManager } from '../src/card-controller/download-manager';
|
||||
import { ExpandManager } from '../src/card-controller/expand-manager';
|
||||
import { FullscreenManager } from '../src/card-controller/fullscreen-manager';
|
||||
import { FullscreenManager } from '../src/card-controller/fullscreen/fullscreen-manager';
|
||||
import { HASSManager } from '../src/card-controller/hass/hass-manager';
|
||||
import { StateWatcherSubscriptionInterface } from '../src/card-controller/hass/state-watcher';
|
||||
import { InitializationManager } from '../src/card-controller/initialization-manager';
|
||||
@@ -535,3 +536,7 @@ export const createInteractionEvent = (
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const setScreenfulEnabled = (enabled: boolean): void => {
|
||||
Object.defineProperty(screenfull, 'isEnabled', { value: enabled, writable: true });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user