@@ -37,7 +37,7 @@ menu:
|
|||||||
| `download` | The `download` menu button: allow direct download of the media being displayed. |
|
| `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. |
|
| `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` . |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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
|
### Fullscreen doesn't work on iPhone
|
||||||
|
|
||||||
Unfortunately, [iOS does not support the Javascript fullscreen
|
Unfortunately, [iOS does not support the Javascript fullscreen
|
||||||
API](https://caniuse.com/fullscreen). As a result, card-level fullscreen support
|
API](https://caniuse.com/fullscreen) on the iPhone, which severely limits the
|
||||||
for the iPhone is not currently possible.
|
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
|
### iOS App not updating after card version change
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class CardElementManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this._api.getConditionsManager()?.setState({
|
this._api.getConditionsManager()?.setState({
|
||||||
user_agent: navigator.userAgent,
|
userAgent: navigator.userAgent,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manually request query string execute as the card will be
|
// Manually request query string execute as the card will be
|
||||||
|
|||||||
@@ -17,25 +17,25 @@ import {
|
|||||||
ViewDisplayMode,
|
ViewDisplayMode,
|
||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import { FrigateCardError } from '../types';
|
import { FrigateCardError, MediaLoadedInfo } from '../types';
|
||||||
import { desparsifyArrays } from '../utils/basic';
|
import { desparsifyArrays } from '../utils/basic';
|
||||||
import { isCompanionApp } from '../utils/companion';
|
import { isCompanionApp } from '../utils/companion';
|
||||||
import { CardConditionAPI, KeysState, MicrophoneState } from './types';
|
import { CardConditionAPI, KeysState, MicrophoneState } from './types';
|
||||||
|
|
||||||
interface ConditionState {
|
export interface ConditionState {
|
||||||
view?: string;
|
|
||||||
fullscreen?: boolean;
|
|
||||||
expand?: boolean;
|
|
||||||
camera?: string;
|
camera?: string;
|
||||||
state?: HassEntities;
|
|
||||||
media_loaded?: boolean;
|
|
||||||
displayMode?: ViewDisplayMode;
|
displayMode?: ViewDisplayMode;
|
||||||
triggered?: Set<string>;
|
expand?: boolean;
|
||||||
|
fullscreen?: boolean;
|
||||||
interaction?: boolean;
|
interaction?: boolean;
|
||||||
microphone?: MicrophoneState;
|
|
||||||
user?: CurrentUser;
|
|
||||||
keys?: KeysState;
|
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 {}
|
class OverrideConfigurationError extends FrigateCardError {}
|
||||||
@@ -152,7 +152,10 @@ export interface ConditionsManagerEpoch {
|
|||||||
manager: Readonly<ConditionsManager>;
|
manager: Readonly<ConditionsManager>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConditionsManagerListener = () => void;
|
export type ConditionsManagerListener = (
|
||||||
|
newState: ConditionState,
|
||||||
|
oldState?: ConditionState,
|
||||||
|
) => void;
|
||||||
|
|
||||||
export class ConditionsManager {
|
export class ConditionsManager {
|
||||||
protected _api: CardConditionAPI;
|
protected _api: CardConditionAPI;
|
||||||
@@ -166,7 +169,7 @@ export class ConditionsManager {
|
|||||||
// the configuration needs to consume it.
|
// the configuration needs to consume it.
|
||||||
protected _hasHAStateConditions = false;
|
protected _hasHAStateConditions = false;
|
||||||
protected _mediaQueries: MediaQueryList[] = [];
|
protected _mediaQueries: MediaQueryList[] = [];
|
||||||
protected _mediaQueryTrigger = () => this._triggerChange();
|
protected _mediaQueryTrigger = () => this._triggerChange(this._state);
|
||||||
|
|
||||||
constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) {
|
constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) {
|
||||||
this._api = api;
|
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 {
|
public removeConditions(): void {
|
||||||
this._mediaQueries.forEach((mql) =>
|
this._mediaQueries.forEach((mql) =>
|
||||||
mql.removeEventListener('change', this._mediaQueryTrigger),
|
mql.removeEventListener('change', this._mediaQueryTrigger),
|
||||||
@@ -233,12 +244,12 @@ export class ConditionsManager {
|
|||||||
if (Object.keys(state).every((key) => isEqual(state[key], this._state[key]))) {
|
if (Object.keys(state).every((key) => isEqual(state[key], this._state[key]))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const oldState = this._state;
|
||||||
this._state = {
|
this._state = {
|
||||||
...this._state,
|
...oldState,
|
||||||
...state,
|
...state,
|
||||||
};
|
};
|
||||||
this._triggerChange();
|
this._triggerChange(oldState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getState(): ConditionState {
|
public getState(): ConditionState {
|
||||||
@@ -313,8 +324,8 @@ export class ConditionsManager {
|
|||||||
return !!state.user && conditionObj.users.includes(state.user.id);
|
return !!state.user && conditionObj.users.includes(state.user.id);
|
||||||
case 'media_loaded':
|
case 'media_loaded':
|
||||||
return (
|
return (
|
||||||
state.media_loaded !== undefined &&
|
state.mediaLoadedInfo !== undefined &&
|
||||||
conditionObj.media_loaded === state.media_loaded
|
conditionObj.media_loaded === !!state.mediaLoadedInfo
|
||||||
);
|
);
|
||||||
case 'screen':
|
case 'screen':
|
||||||
return window.matchMedia(conditionObj.media_query).matches;
|
return window.matchMedia(conditionObj.media_query).matches;
|
||||||
@@ -352,12 +363,12 @@ export class ConditionsManager {
|
|||||||
);
|
);
|
||||||
case 'user_agent':
|
case 'user_agent':
|
||||||
return (
|
return (
|
||||||
!!state.user_agent &&
|
!!state.userAgent &&
|
||||||
(!conditionObj.user_agent || conditionObj.user_agent === state.user_agent) &&
|
(!conditionObj.user_agent || conditionObj.user_agent === state.userAgent) &&
|
||||||
(conditionObj.companion === undefined ||
|
(conditionObj.companion === undefined ||
|
||||||
conditionObj.companion === isCompanionApp(state.user_agent)) &&
|
conditionObj.companion === isCompanionApp(state.userAgent)) &&
|
||||||
(conditionObj.user_agent_re === undefined ||
|
(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 };
|
return { manager: this };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _triggerChange(): void {
|
protected _triggerChange(oldState?: ConditionState): void {
|
||||||
this._epoch = this._createEpoch();
|
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 { DefaultManager } from './default-manager';
|
||||||
import { DownloadManager } from './download-manager';
|
import { DownloadManager } from './download-manager';
|
||||||
import { ExpandManager } from './expand-manager';
|
import { ExpandManager } from './expand-manager';
|
||||||
import { FullscreenManager } from './fullscreen-manager';
|
import { FullscreenManager } from './fullscreen/fullscreen-manager';
|
||||||
import { HASSManager } from './hass/hass-manager';
|
import { HASSManager } from './hass/hass-manager';
|
||||||
import { InitializationManager } from './initialization-manager';
|
import { InitializationManager } from './initialization-manager';
|
||||||
import { InteractionManager } from './interaction-manager';
|
import { InteractionManager } from './interaction-manager';
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class ExpandManager {
|
|||||||
public setExpanded(expanded: boolean): void {
|
public setExpanded(expanded: boolean): void {
|
||||||
if (expanded && this._api.getFullscreenManager().isInFullscreen()) {
|
if (expanded && this._api.getFullscreenManager().isInFullscreen()) {
|
||||||
// Fullscreen and expanded mode are mutually exclusive.
|
// Fullscreen and expanded mode are mutually exclusive.
|
||||||
this._api.getFullscreenManager().stopFullscreen();
|
this._api.getFullscreenManager().setFullscreen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._expanded = expanded;
|
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();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public set(mediaInfo: MediaLoadedInfo): void {
|
public set(mediaLoadedInfo: MediaLoadedInfo): void {
|
||||||
if (!isValidMediaLoadedInfo(mediaInfo)) {
|
if (!isValidMediaLoadedInfo(mediaLoadedInfo)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
this._api.getConfigManager().getCardWideConfig(),
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
`Frigate Card media load: `,
|
`Frigate Card media load: `,
|
||||||
mediaInfo,
|
mediaLoadedInfo,
|
||||||
);
|
);
|
||||||
|
|
||||||
this._current = mediaInfo;
|
this._current = mediaLoadedInfo;
|
||||||
this._lastKnown = mediaInfo;
|
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.
|
// Fresh media information may change how the card is rendered.
|
||||||
this._api.getStyleManager().setExpandedMode();
|
this._api.getStyleManager().setExpandedMode();
|
||||||
@@ -47,7 +47,7 @@ export class MediaLoadedInfoManager {
|
|||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
this._current = null;
|
this._current = null;
|
||||||
this._api.getConditionsManager().setState({ media_loaded: false });
|
this._api.getConditionsManager().setState({ mediaLoadedInfo: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
public has(): boolean {
|
public has(): boolean {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type { ConfigManager } from './config/config-manager';
|
|||||||
import type { DefaultManager } from './default-manager';
|
import type { DefaultManager } from './default-manager';
|
||||||
import type { DownloadManager } from './download-manager';
|
import type { DownloadManager } from './download-manager';
|
||||||
import type { ExpandManager } from './expand-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 { HASSManager } from './hass/hass-manager';
|
||||||
import type { InitializationManager } from './initialization-manager';
|
import type { InitializationManager } from './initialization-manager';
|
||||||
import type { InteractionManager } from './interaction-manager';
|
import type { InteractionManager } from './interaction-manager';
|
||||||
@@ -150,6 +150,7 @@ export interface CardFullscreenAPI {
|
|||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionsManager(): ConditionsManager;
|
||||||
getExpandManager(): ExpandManager;
|
getExpandManager(): ExpandManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
getMediaPlayerManager(): MediaPlayerManager;
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +206,7 @@ export interface CardMediaLoadedAPI {
|
|||||||
getCardElementManager(): CardElementManager;
|
getCardElementManager(): CardElementManager;
|
||||||
getConditionsManager(): ConditionsManager;
|
getConditionsManager(): ConditionsManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
|
getFullscreenManager(): FullscreenManager;
|
||||||
getStyleManager(): StyleManager;
|
getStyleManager(): StyleManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -270,7 +270,7 @@ class FrigateCard extends LitElement {
|
|||||||
this._controller.getCameraManager(),
|
this._controller.getCameraManager(),
|
||||||
{
|
{
|
||||||
inExpandedMode: this._controller.getExpandManager().isExpanded(),
|
inExpandedMode: this._controller.getExpandManager().isExpanded(),
|
||||||
inFullscreenMode: this._controller.getFullscreenManager().isInFullscreen(),
|
fullscreenManager: this._controller.getFullscreenManager(),
|
||||||
currentMediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
currentMediaLoadedInfo: this._controller.getMediaLoadedInfoManager().get(),
|
||||||
showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(),
|
showCameraUIButton: this._controller.getCameraURLManager().hasCameraURL(),
|
||||||
mediaPlayerController: this._controller.getMediaPlayerManager(),
|
mediaPlayerController: this._controller.getMediaPlayerManager(),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
import { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import { StyleInfo } from 'lit/directives/style-map';
|
import { StyleInfo } from 'lit/directives/style-map';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { FullscreenManager } from '../card-controller/fullscreen/fullscreen-manager';
|
||||||
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
||||||
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||||
import { ViewManager } from '../card-controller/view/view-manager';
|
import { ViewManager } from '../card-controller/view/view-manager';
|
||||||
@@ -32,7 +33,7 @@ import { getCameraIDsForViewName } from '../view/view-to-cameras';
|
|||||||
export interface MenuButtonControllerOptions {
|
export interface MenuButtonControllerOptions {
|
||||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
showCameraUIButton?: boolean;
|
showCameraUIButton?: boolean;
|
||||||
inFullscreenMode?: boolean;
|
fullscreenManager?: FullscreenManager | null;
|
||||||
inExpandedMode?: boolean;
|
inExpandedMode?: boolean;
|
||||||
microphoneManager?: MicrophoneManager | null;
|
microphoneManager?: MicrophoneManager | null;
|
||||||
mediaPlayerController?: MediaPlayerManager | null;
|
mediaPlayerController?: MediaPlayerManager | null;
|
||||||
@@ -84,7 +85,7 @@ export class MenuButtonController {
|
|||||||
options?.currentMediaLoadedInfo,
|
options?.currentMediaLoadedInfo,
|
||||||
),
|
),
|
||||||
this._getExpandButton(config, options?.inExpandedMode),
|
this._getExpandButton(config, options?.inExpandedMode),
|
||||||
this._getFullscreenButton(config, options?.inFullscreenMode),
|
this._getFullscreenButton(config, options?.fullscreenManager),
|
||||||
this._getCastButton(
|
this._getCastButton(
|
||||||
hass,
|
hass,
|
||||||
config,
|
config,
|
||||||
@@ -420,16 +421,17 @@ export class MenuButtonController {
|
|||||||
|
|
||||||
protected _getFullscreenButton(
|
protected _getFullscreenButton(
|
||||||
config: FrigateCardConfig,
|
config: FrigateCardConfig,
|
||||||
inFullscreenMode?: boolean,
|
fullscreenManager?: FullscreenManager | null,
|
||||||
): MenuItem | 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,
|
...config.menu.buttons.fullscreen,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.fullscreen'),
|
title: localize('config.menu.buttons.fullscreen'),
|
||||||
tap_action: createGeneralAction('fullscreen') as FrigateCardCustomAction,
|
tap_action: createGeneralAction('fullscreen') as FrigateCardCustomAction,
|
||||||
style: inFullscreenMode ? this._getEmphasizedStyle() : {},
|
style: inFullscreen ? this._getEmphasizedStyle() : {},
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -694,7 +696,7 @@ export class MenuButtonController {
|
|||||||
(frigateCardAction.frigate_card_action === 'default' &&
|
(frigateCardAction.frigate_card_action === 'default' &&
|
||||||
options?.view?.is(config.view.default)) ||
|
options?.view?.is(config.view.default)) ||
|
||||||
(frigateCardAction.frigate_card_action === 'fullscreen' &&
|
(frigateCardAction.frigate_card_action === 'fullscreen' &&
|
||||||
!!options?.inFullscreenMode) ||
|
!!options?.fullscreenManager?.isInFullscreen()) ||
|
||||||
(frigateCardAction.frigate_card_action === 'camera_select' &&
|
(frigateCardAction.frigate_card_action === 'camera_select' &&
|
||||||
options?.view?.camera === frigateCardAction.camera)
|
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 defaultImage from '../images/frigate-bird-in-sky.jpg';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import imageStyle from '../scss/image.scss';
|
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 { contentsChanged } from '../utils/basic.js';
|
||||||
import { isHassDifferent } from '../utils/ha/index.js';
|
import { isHassDifferent } from '../utils/ha/index.js';
|
||||||
import {
|
import {
|
||||||
@@ -123,6 +128,10 @@ export class FrigateCardImageBase extends LitElement implements FrigateCardMedia
|
|||||||
return this._cachedValueController?.value ?? null;
|
return this._cachedValueController?.value ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._refImage.value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the element should be updated.
|
* Determine whether the element should be updated.
|
||||||
* @param changedProps The changed properties if any.
|
* @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 { CameraConfig, ImageViewConfig } from '../config/types';
|
||||||
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
import { IMAGE_VIEW_ZOOM_TARGET_SENTINEL } from '../const';
|
||||||
import basicBlockStyle from '../scss/basic-block.scss';
|
import basicBlockStyle from '../scss/basic-block.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../types.js';
|
import { FrigateCardMediaPlayer, FullscreenElement } from '../types.js';
|
||||||
import { aspectRatioToString } from '../utils/basic';
|
import { aspectRatioToString } from '../utils/basic';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||||
import './image-base';
|
import './image-base';
|
||||||
@@ -79,6 +79,10 @@ export class FrigateCardImage extends LitElement implements FrigateCardMediaPlay
|
|||||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._refImage.value?.getFullscreenElement() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
if (changedProps.has('cameraConfig') || changedProps.has('imageConfig')) {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ import {
|
|||||||
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
import { STREAM_TROUBLESHOOTING_URL } from '../../const.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
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 { aspectRatioToString } from '../../utils/basic.js';
|
||||||
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
import { dispatchMediaUnloadedEvent } from '../../utils/media-info.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../../utils/media-layout.js';
|
||||||
@@ -144,6 +148,10 @@ export class FrigateCardLiveProvider
|
|||||||
return (await this._refProvider.value?.getScreenshotURL()) ?? null;
|
return (await this._refProvider.value?.getScreenshotURL()) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._refProvider.value?.getFullscreenElement() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the fully resolved live provider.
|
* Get the fully resolved live provider.
|
||||||
* @returns A live provider (that is not 'auto').
|
* @returns A live provider (that is not 'auto').
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import liveGo2RTCStyle from '../../../../scss/live-go2rtc.scss';
|
|||||||
import {
|
import {
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
|
FullscreenElement,
|
||||||
Message,
|
Message,
|
||||||
} from '../../../../types.js';
|
} from '../../../../types.js';
|
||||||
import { convertEndpointAddressToSignedWebsocket } from '../../../../utils/endpoint.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;
|
return this._player?.video ? screenshotMedia(this._player.video) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._player?.video ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
disconnectedCallback(): void {
|
disconnectedCallback(): void {
|
||||||
this._player = undefined;
|
this._player = undefined;
|
||||||
this._message = null;
|
this._message = null;
|
||||||
@@ -151,7 +156,7 @@ export class FrigateCardGo2RTC extends LitElement implements FrigateCardMediaPla
|
|||||||
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
this._player.microphoneStream = this.microphoneState?.stream ?? null;
|
||||||
this._player.src = address;
|
this._player.src = address;
|
||||||
this._player.visibilityCheck = false;
|
this._player.visibilityCheck = false;
|
||||||
this._player.controls = this.controls;
|
this._player.setControls(this.controls);
|
||||||
|
|
||||||
if (this.cameraConfig?.go2rtc?.modes && this.cameraConfig.go2rtc.modes.length) {
|
if (this.cameraConfig?.go2rtc?.modes && this.cameraConfig.go2rtc.modes.length) {
|
||||||
this._player.mode = this.cameraConfig.go2rtc.modes.join(',');
|
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) {
|
if (changedProps.has('controls') && this._player) {
|
||||||
this._player.controls = this.controls;
|
this._player.setControls(this.controls);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
+2
-1
@@ -29,8 +29,9 @@ export class VideoRTC extends HTMLElement {
|
|||||||
onmessage: Record<string, (msg: { type: string; value: string }) => void>;
|
onmessage: Record<string, (msg: { type: string; value: string }) => void>;
|
||||||
|
|
||||||
// Custom methods/members.
|
// Custom methods/members.
|
||||||
controls: boolean;
|
|
||||||
containingPlayer: FrigateCardMediaPlayer | null;
|
containingPlayer: FrigateCardMediaPlayer | null;
|
||||||
microphoneStream: MediaStream | null;
|
microphoneStream: MediaStream | null;
|
||||||
reconnect();
|
reconnect();
|
||||||
|
|
||||||
|
setControls(controls: boolean): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export class VideoRTC extends HTMLElement {
|
|||||||
this.containingPlayer = null;
|
this.containingPlayer = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether to show or hide video controls.
|
* Whether to show or hide video controls for videos created in future.
|
||||||
* @type {boolean}}
|
* @type {boolean}}
|
||||||
*/
|
*/
|
||||||
this.controls = true;
|
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.
|
* Set video source (WebSocket URL). Support relative path.
|
||||||
* @param {string|URL} value
|
* @param {string|URL} value
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import '../../../patches/ha-camera-stream';
|
|||||||
import '../../../patches/ha-hls-player.js';
|
import '../../../patches/ha-hls-player.js';
|
||||||
import '../../../patches/ha-web-rtc-player.js';
|
import '../../../patches/ha-web-rtc-player.js';
|
||||||
import liveHAStyle from '../../../scss/live-ha.scss';
|
import liveHAStyle from '../../../scss/live-ha.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../../../types.js';
|
import { FrigateCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
||||||
|
|
||||||
@customElement('frigate-card-live-ha')
|
@customElement('frigate-card-live-ha')
|
||||||
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
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;
|
return (await this._playerRef.value?.getScreenshotURL()) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._playerRef.value?.getFullscreenElement() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { CameraConfig } from '../../../config/types';
|
import { CameraConfig } from '../../../config/types';
|
||||||
import basicBlockStyle from '../../../scss/basic-block.scss';
|
import basicBlockStyle from '../../../scss/basic-block.scss';
|
||||||
import { FrigateCardMediaPlayer } from '../../../types.js';
|
import { FrigateCardMediaPlayer, FullscreenElement } from '../../../types.js';
|
||||||
import '../../image-base.js';
|
import '../../image-base.js';
|
||||||
|
|
||||||
@customElement('frigate-card-live-image')
|
@customElement('frigate-card-live-image')
|
||||||
@@ -53,6 +53,10 @@ export class FrigateCardLiveImage extends LitElement implements FrigateCardMedia
|
|||||||
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
return (await this._refImage.value?.getScreenshotURL()) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._refImage.value?.getFullscreenElement() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.cameraConfig) {
|
if (!this.hass || !this.cameraConfig) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import liveJSMPEGStyle from '../../../scss/live-jsmpeg.scss';
|
|||||||
import {
|
import {
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
|
FullscreenElement,
|
||||||
Message,
|
Message,
|
||||||
} from '../../../types.js';
|
} from '../../../types.js';
|
||||||
import { convertEndpointAddressToSignedWebsocket } from '../../../utils/endpoint.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;
|
return this._jsmpegCanvasElement?.toDataURL('image/jpeg') ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._jsmpegCanvasElement ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProperties: PropertyValues): void {
|
protected willUpdate(changedProperties: PropertyValues): void {
|
||||||
if (
|
if (
|
||||||
['cameraConfig', 'cameraEndpoints'].some((prop) => changedProperties.has(prop))
|
['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 { CameraConfig, CardWideConfig } from '../../../config/types.js';
|
||||||
import { localize } from '../../../localize/localize.js';
|
import { localize } from '../../../localize/localize.js';
|
||||||
import liveWebRTCCardStyle from '../../../scss/live-webrtc-card.scss';
|
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 { mayHaveAudio } from '../../../utils/audio.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
@@ -64,54 +69,58 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
protected _webrtcTask = new Task(this, this._getWebRTCCardElement, () => [1]);
|
||||||
|
|
||||||
public async play(): Promise<void> {
|
public async play(): Promise<void> {
|
||||||
return this._getPlayer()?.play();
|
return this._getVideo()?.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async pause(): Promise<void> {
|
public async pause(): Promise<void> {
|
||||||
this._getPlayer()?.pause();
|
this._getVideo()?.pause();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async mute(): Promise<void> {
|
public async mute(): Promise<void> {
|
||||||
const player = this._getPlayer();
|
const player = this._getVideo();
|
||||||
if (player) {
|
if (player) {
|
||||||
player.muted = true;
|
player.muted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
public async unmute(): Promise<void> {
|
||||||
const player = this._getPlayer();
|
const player = this._getVideo();
|
||||||
if (player) {
|
if (player) {
|
||||||
player.muted = false;
|
player.muted = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isMuted(): boolean {
|
public isMuted(): boolean {
|
||||||
return this._getPlayer()?.muted ?? true;
|
return this._getVideo()?.muted ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async seek(seconds: number): Promise<void> {
|
public async seek(seconds: number): Promise<void> {
|
||||||
const player = this._getPlayer();
|
const player = this._getVideo();
|
||||||
if (player) {
|
if (player) {
|
||||||
player.currentTime = seconds;
|
player.currentTime = seconds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setControls(controls?: boolean): Promise<void> {
|
public async setControls(controls?: boolean): Promise<void> {
|
||||||
const player = this._getPlayer();
|
const player = this._getVideo();
|
||||||
if (player) {
|
if (player) {
|
||||||
setControlsOnVideo(player, controls ?? this.controls);
|
setControlsOnVideo(player, controls ?? this.controls);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isPaused(): boolean {
|
public isPaused(): boolean {
|
||||||
return this._getPlayer()?.paused ?? true;
|
return this._getVideo()?.paused ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
public async getScreenshotURL(): Promise<string | null> {
|
||||||
const video = this._getPlayer();
|
const video = this._getVideo();
|
||||||
return video ? screenshotMedia(video) : null;
|
return video ? screenshotMedia(video) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._getVideo();
|
||||||
|
}
|
||||||
|
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
super.connectedCallback();
|
super.connectedCallback();
|
||||||
|
|
||||||
@@ -141,7 +150,7 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
* Get the underlying video player.
|
* Get the underlying video player.
|
||||||
* @returns The player or `null` if not found.
|
* @returns The player or `null` if not found.
|
||||||
*/
|
*/
|
||||||
protected _getPlayer(): HTMLVideoElement | null {
|
protected _getVideo(): HTMLVideoElement | null {
|
||||||
return this._getVideoRTC()?.video ?? null;
|
return this._getVideoRTC()?.video ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,7 +245,7 @@ export class FrigateCardLiveWebRTCCard
|
|||||||
// media load event.
|
// media load event.
|
||||||
this.updateComplete.then(() => {
|
this.updateComplete.then(() => {
|
||||||
const videoRTC = this._getVideoRTC();
|
const videoRTC = this._getVideoRTC();
|
||||||
const video = this._getPlayer();
|
const video = this._getVideo();
|
||||||
if (video) {
|
if (video) {
|
||||||
setControlsOnVideo(video, this.controls);
|
setControlsOnVideo(video, this.controls);
|
||||||
video.onloadeddata = () => {
|
video.onloadeddata = () => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import viewerProviderStyle from '../../scss/viewer-provider.scss';
|
|||||||
import {
|
import {
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
|
FullscreenElement,
|
||||||
ResolvedMedia,
|
ResolvedMedia,
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { mayHaveAudio } from '../../utils/audio.js';
|
import { mayHaveAudio } from '../../utils/audio.js';
|
||||||
@@ -169,6 +170,15 @@ export class FrigateCardViewerProvider
|
|||||||
return null;
|
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> {
|
protected async _switchToRelatedClipView(): Promise<void> {
|
||||||
const view = this.viewManagerEpoch?.manager.getView();
|
const view = this.viewManagerEpoch?.manager.getView();
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -12,14 +12,14 @@
|
|||||||
import { css, CSSResultGroup, html, nothing, PropertyValues, unsafeCSS } from 'lit';
|
import { css, CSSResultGroup, html, nothing, PropertyValues, unsafeCSS } from 'lit';
|
||||||
import { customElement } from 'lit/decorators.js';
|
import { customElement } from 'lit/decorators.js';
|
||||||
import { query } from 'lit/decorators/query.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 {
|
import {
|
||||||
createMediaLoadedInfo,
|
createMediaLoadedInfo,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
} from '../utils/media-info.js';
|
} from '../utils/media-info.js';
|
||||||
import './ha-hls-player';
|
import './ha-hls-player';
|
||||||
import './ha-web-rtc-player';
|
import './ha-web-rtc-player';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
|
||||||
|
|
||||||
customElements.whenDefined('ha-camera-stream').then(() => {
|
customElements.whenDefined('ha-camera-stream').then(() => {
|
||||||
// ========================================================================================
|
// ========================================================================================
|
||||||
@@ -93,6 +93,10 @@ customElements.whenDefined('ha-camera-stream').then(() => {
|
|||||||
return this._player ? await this._player.getScreenshotURL() : null;
|
return this._player ? await this._player.getScreenshotURL() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._player?.getFullscreenElement() ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
protected _storeMediaLoadedInfoHandler(
|
protected _storeMediaLoadedInfoHandler(
|
||||||
stream: StreamType,
|
stream: StreamType,
|
||||||
ev: CustomEvent<MediaLoadedInfo>,
|
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 { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { renderMessage } from '../components/message.js';
|
import { renderMessage } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
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 { mayHaveAudio } from '../utils/audio.js';
|
||||||
import { errorToConsole } from '../utils/basic.js';
|
import { errorToConsole } from '../utils/basic.js';
|
||||||
import {
|
import {
|
||||||
@@ -91,6 +91,10 @@ customElements.whenDefined('ha-hls-player').then(() => {
|
|||||||
return this._video ? screenshotMedia(this._video) : null;
|
return this._video ? screenshotMedia(this._video) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._video;
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// Minor modifications from:
|
// Minor modifications from:
|
||||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-hls-player.ts
|
// - 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 { dispatchLiveErrorEvent } from '../components-lib/live/utils/dispatch-live-error.js';
|
||||||
import { renderMessage } from '../components/message.js';
|
import { renderMessage } from '../components/message.js';
|
||||||
import liveHAComponentsStyle from '../scss/live-ha-components.scss';
|
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 { mayHaveAudio } from '../utils/audio.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
@@ -90,6 +90,10 @@ customElements.whenDefined('ha-web-rtc-player').then(() => {
|
|||||||
return this._video ? screenshotMedia(this._video) : null;
|
return this._video ? screenshotMedia(this._video) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getFullscreenElement(): FullscreenElement | null {
|
||||||
|
return this._video ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
// =====================================================================================
|
// =====================================================================================
|
||||||
// Minor modifications from:
|
// Minor modifications from:
|
||||||
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-web-rtc-player.ts
|
// - https://github.com/home-assistant/frontend/blob/dev/src/components/ha-web-rtc-player.ts
|
||||||
|
|||||||
@@ -54,6 +54,15 @@ export interface Message {
|
|||||||
troubleshootingURL?: string;
|
troubleshootingURL?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WebkitHTMLVideoElement = HTMLVideoElement & {
|
||||||
|
webkitDisplayingFullscreen: boolean;
|
||||||
|
webkitSupportsFullscreen: boolean;
|
||||||
|
webkitEnterFullscreen: () => void;
|
||||||
|
webkitExitFullscreen: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FullscreenElement = HTMLElement;
|
||||||
|
|
||||||
export interface FrigateCardMediaPlayer {
|
export interface FrigateCardMediaPlayer {
|
||||||
play(): Promise<void>;
|
play(): Promise<void>;
|
||||||
pause(): 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.
|
// If no value for controls if specified, the player should use the default.
|
||||||
setControls(controls?: boolean): Promise<void>;
|
setControls(controls?: boolean): Promise<void>;
|
||||||
isPaused(): boolean;
|
isPaused(): boolean;
|
||||||
|
getFullscreenElement(): FullscreenElement | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LovelaceCardWithEditor = LovelaceCard & {
|
export type LovelaceCardWithEditor = LovelaceCard & {
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const getReleaseVersion = (): string => {
|
|||||||
return releaseVersion;
|
return releaseVersion;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Diagnostics {
|
interface Diagnostics {
|
||||||
card_version: string;
|
card_version: string;
|
||||||
browser: string;
|
browser: string;
|
||||||
date: Date;
|
date: Date;
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import {
|
|||||||
evaluateConditionViaEvent,
|
evaluateConditionViaEvent,
|
||||||
getOverriddenConfig,
|
getOverriddenConfig,
|
||||||
} from '../../src/card-controller/conditions-manager';
|
} from '../../src/card-controller/conditions-manager';
|
||||||
|
import { MicrophoneState } from '../../src/card-controller/types';
|
||||||
import { FrigateCardCondition } from '../../src/config/types';
|
import { FrigateCardCondition } from '../../src/config/types';
|
||||||
import {
|
import {
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
createCondition,
|
createCondition,
|
||||||
createConfig,
|
createConfig,
|
||||||
|
createMediaLoadedInfo,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
createUser,
|
createUser,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
@@ -780,9 +782,9 @@ describe('ConditionsManager', () => {
|
|||||||
const manager = new ConditionsManager(createCardAPI());
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
const conditions = [{ condition: 'media_loaded' as const, media_loaded: true }];
|
const conditions = [{ condition: 'media_loaded' as const, media_loaded: true }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ media_loaded: true });
|
manager.setState({ mediaLoadedInfo: createMediaLoadedInfo() });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({ media_loaded: false });
|
manager.setState({ mediaLoadedInfo: null });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -898,17 +900,27 @@ describe('ConditionsManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('with microphone condition', () => {
|
describe('with microphone condition', () => {
|
||||||
|
const createMicrophoneState = (
|
||||||
|
state: Partial<MicrophoneState>,
|
||||||
|
): MicrophoneState => {
|
||||||
|
return {
|
||||||
|
connected: false,
|
||||||
|
muted: false,
|
||||||
|
forbidden: false,
|
||||||
|
...state,
|
||||||
|
};
|
||||||
|
};
|
||||||
it('empty', () => {
|
it('empty', () => {
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
const conditions = [{ condition: 'microphone' as const }];
|
const conditions = [{ condition: 'microphone' as const }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({ microphone: { connected: true } });
|
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({ microphone: { connected: false } });
|
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({ microphone: { muted: true } });
|
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({ microphone: { muted: false } });
|
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -916,9 +928,9 @@ describe('ConditionsManager', () => {
|
|||||||
const manager = new ConditionsManager(createCardAPI());
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
const conditions = [{ condition: 'microphone' as const, connected: true }];
|
const conditions = [{ condition: 'microphone' as const, connected: true }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { connected: true } });
|
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({ microphone: { connected: false } });
|
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -926,9 +938,9 @@ describe('ConditionsManager', () => {
|
|||||||
const manager = new ConditionsManager(createCardAPI());
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
const conditions = [{ condition: 'microphone' as const, connected: false }];
|
const conditions = [{ condition: 'microphone' as const, connected: false }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { connected: true } });
|
manager.setState({ microphone: createMicrophoneState({ connected: true }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { connected: false } });
|
manager.setState({ microphone: createMicrophoneState({ connected: false }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -936,9 +948,9 @@ describe('ConditionsManager', () => {
|
|||||||
const manager = new ConditionsManager(createCardAPI());
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
const conditions = [{ condition: 'microphone' as const, muted: true }];
|
const conditions = [{ condition: 'microphone' as const, muted: true }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { muted: true } });
|
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({ microphone: { muted: false } });
|
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -946,9 +958,9 @@ describe('ConditionsManager', () => {
|
|||||||
const manager = new ConditionsManager(createCardAPI());
|
const manager = new ConditionsManager(createCardAPI());
|
||||||
const conditions = [{ condition: 'microphone' as const, muted: false }];
|
const conditions = [{ condition: 'microphone' as const, muted: false }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { muted: true } });
|
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { muted: false } });
|
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -958,13 +970,17 @@ describe('ConditionsManager', () => {
|
|||||||
{ condition: 'microphone' as const, muted: false, connected: true },
|
{ condition: 'microphone' as const, muted: false, connected: true },
|
||||||
];
|
];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { muted: true } });
|
manager.setState({ microphone: createMicrophoneState({ muted: true }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({ microphone: { muted: false } });
|
manager.setState({ microphone: createMicrophoneState({ muted: false }) });
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
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();
|
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();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1033,11 +1049,11 @@ describe('ConditionsManager', () => {
|
|||||||
const conditions = [{ condition: 'user_agent' as const, user_agent: userAgent }];
|
const conditions = [{ condition: 'user_agent' as const, user_agent: userAgent }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: userAgent,
|
userAgent: userAgent,
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: 'Something else',
|
userAgent: 'Something else',
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
});
|
});
|
||||||
@@ -1049,11 +1065,11 @@ describe('ConditionsManager', () => {
|
|||||||
];
|
];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: userAgent,
|
userAgent: userAgent,
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: 'Something else',
|
userAgent: 'Something else',
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
});
|
});
|
||||||
@@ -1063,11 +1079,11 @@ describe('ConditionsManager', () => {
|
|||||||
const conditions = [{ condition: 'user_agent' as const, companion: true }];
|
const conditions = [{ condition: 'user_agent' as const, companion: true }];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: 'Home Assistant/',
|
userAgent: 'Home Assistant/',
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: userAgent,
|
userAgent: userAgent,
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
});
|
});
|
||||||
@@ -1084,14 +1100,37 @@ describe('ConditionsManager', () => {
|
|||||||
];
|
];
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: 'Home Assistant/',
|
userAgent: 'Home Assistant/',
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
expect(manager.evaluateConditions(conditions)).toBeTruthy();
|
||||||
manager.setState({
|
manager.setState({
|
||||||
user_agent: 'Something else',
|
userAgent: 'Something else',
|
||||||
});
|
});
|
||||||
expect(manager.evaluateConditions(conditions)).toBeFalsy();
|
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 { DefaultManager } from '../../src/card-controller/default-manager';
|
||||||
import { DownloadManager } from '../../src/card-controller/download-manager';
|
import { DownloadManager } from '../../src/card-controller/download-manager';
|
||||||
import { ExpandManager } from '../../src/card-controller/expand-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 { HASSManager } from '../../src/card-controller/hass/hass-manager';
|
||||||
import { InitializationManager } from '../../src/card-controller/initialization-manager';
|
import { InitializationManager } from '../../src/card-controller/initialization-manager';
|
||||||
import { InteractionManager } from '../../src/card-controller/interaction-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/default-manager');
|
||||||
vi.mock('../../src/card-controller/download-manager');
|
vi.mock('../../src/card-controller/download-manager');
|
||||||
vi.mock('../../src/card-controller/expand-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/hass/hass-manager');
|
||||||
vi.mock('../../src/card-controller/initialization-manager');
|
vi.mock('../../src/card-controller/initialization-manager');
|
||||||
vi.mock('../../src/card-controller/interaction-manager');
|
vi.mock('../../src/card-controller/interaction-manager');
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ describe('ExpandManager', () => {
|
|||||||
manager.setExpanded(true);
|
manager.setExpanded(true);
|
||||||
|
|
||||||
expect(manager.isExpanded()).toBeTruthy();
|
expect(manager.isExpanded()).toBeTruthy();
|
||||||
expect(api.getFullscreenManager().stopFullscreen).toBeCalled();
|
expect(api.getFullscreenManager().setFullscreen).toBeCalledWith(false);
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true });
|
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true });
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
expect(api.getCardElementManager().update).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -37,7 +37,7 @@ describe('ExpandManager', () => {
|
|||||||
|
|
||||||
manager.setExpanded(true);
|
manager.setExpanded(true);
|
||||||
|
|
||||||
expect(api.getFullscreenManager().stopFullscreen).not.toBeCalled();
|
expect(api.getFullscreenManager().setFullscreen).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should toggle expanded', () => {
|
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);
|
const manager = new MediaLoadedInfoManager(api);
|
||||||
|
|
||||||
manager.initialize();
|
manager.initialize();
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({ media_loaded: false });
|
expect(api.getConditionsManager().setState).toBeCalledWith({
|
||||||
|
mediaLoadedInfo: null,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set', () => {
|
it('should set', () => {
|
||||||
@@ -21,7 +23,7 @@ describe('MediaLoadedInfoManager', () => {
|
|||||||
expect(manager.has()).toBeTruthy();
|
expect(manager.has()).toBeTruthy();
|
||||||
expect(manager.get()).toBe(mediaInfo);
|
expect(manager.get()).toBe(mediaInfo);
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
expect(api.getConditionsManager().setState).toBeCalledWith(
|
||||||
expect.objectContaining({ media_loaded: true }),
|
expect.objectContaining({ mediaLoadedInfo: mediaInfo }),
|
||||||
);
|
);
|
||||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
expect(api.getCardElementManager().update).toBeCalled();
|
||||||
@@ -30,9 +32,9 @@ describe('MediaLoadedInfoManager', () => {
|
|||||||
it('should not set invalid media info', () => {
|
it('should not set invalid media info', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new MediaLoadedInfoManager(api);
|
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.has()).toBeFalsy();
|
||||||
expect(manager.get()).toBeNull();
|
expect(manager.get()).toBeNull();
|
||||||
@@ -42,18 +44,18 @@ describe('MediaLoadedInfoManager', () => {
|
|||||||
it('should get last known', () => {
|
it('should get last known', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new MediaLoadedInfoManager(api);
|
const manager = new MediaLoadedInfoManager(api);
|
||||||
const mediaInfo = createMediaLoadedInfo();
|
const mediaLoadedInfo = createMediaLoadedInfo();
|
||||||
|
|
||||||
manager.set(mediaInfo);
|
manager.set(mediaLoadedInfo);
|
||||||
|
|
||||||
expect(manager.has()).toBeTruthy();
|
expect(manager.has()).toBeTruthy();
|
||||||
|
|
||||||
manager.clear();
|
manager.clear();
|
||||||
|
|
||||||
expect(manager.has()).toBeFalsy();
|
expect(manager.has()).toBeFalsy();
|
||||||
expect(manager.getLastKnown()).toBe(mediaInfo);
|
expect(manager.getLastKnown()).toBe(mediaLoadedInfo);
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
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 { HomeAssistant } from '@dermotduffy/custom-card-helpers';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
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 { mock } from 'vitest-mock-extended';
|
||||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||||
import { CameraManager } from '../../src/camera-manager/manager';
|
import { CameraManager } from '../../src/camera-manager/manager';
|
||||||
import { CameraManagerCameraMetadata } from '../../src/camera-manager/types';
|
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 { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
||||||
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
||||||
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
import { ViewManager } from '../../src/card-controller/view/view-manager';
|
||||||
@@ -1121,10 +1122,19 @@ describe('MenuButtonController', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('should have fullscreen button', () => {
|
describe('should have fullscreen button', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals();
|
||||||
|
});
|
||||||
|
|
||||||
it('when not in fullscreen mode', () => {
|
it('when not in fullscreen mode', () => {
|
||||||
// Need to write a readonly property.
|
// Need to write a readonly property.
|
||||||
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
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({
|
expect(buttons).toContainEqual({
|
||||||
icon: 'mdi:fullscreen',
|
icon: 'mdi:fullscreen',
|
||||||
@@ -1139,7 +1149,11 @@ describe('MenuButtonController', () => {
|
|||||||
|
|
||||||
it('when in fullscreen mode', () => {
|
it('when in fullscreen mode', () => {
|
||||||
vi.stubGlobal('navigator', { userAgent: 'foo' });
|
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({
|
expect(buttons).toContainEqual({
|
||||||
icon: 'mdi:fullscreen-exit',
|
icon: 'mdi:fullscreen-exit',
|
||||||
@@ -1151,6 +1165,19 @@ describe('MenuButtonController', () => {
|
|||||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
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', () => {
|
describe('should have expand button', () => {
|
||||||
@@ -1767,8 +1794,11 @@ describe('MenuButtonController', () => {
|
|||||||
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
|
tap_action: { action: 'fire-dom-event', frigate_card_action: 'fullscreen' },
|
||||||
};
|
};
|
||||||
controller.addDynamicMenuButton(button);
|
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,
|
...button,
|
||||||
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
style: { color: 'var(--frigate-card-menu-button-active-color)' },
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-1
@@ -1,6 +1,7 @@
|
|||||||
import { CurrentUser, HASSDomEvent } from '@dermotduffy/custom-card-helpers';
|
import { CurrentUser, HASSDomEvent } from '@dermotduffy/custom-card-helpers';
|
||||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
|
import screenfull from 'screenfull';
|
||||||
import { expect, vi } from 'vitest';
|
import { expect, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { Camera } from '../src/camera-manager/camera';
|
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 { DefaultManager } from '../src/card-controller/default-manager';
|
||||||
import { DownloadManager } from '../src/card-controller/download-manager';
|
import { DownloadManager } from '../src/card-controller/download-manager';
|
||||||
import { ExpandManager } from '../src/card-controller/expand-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 { HASSManager } from '../src/card-controller/hass/hass-manager';
|
||||||
import { StateWatcherSubscriptionInterface } from '../src/card-controller/hass/state-watcher';
|
import { StateWatcherSubscriptionInterface } from '../src/card-controller/hass/state-watcher';
|
||||||
import { InitializationManager } from '../src/card-controller/initialization-manager';
|
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