@@ -128,7 +128,7 @@ export class CardElementManager {
|
||||
);
|
||||
|
||||
this._api.getConditionsManager()?.setState({
|
||||
user_agent: navigator.userAgent,
|
||||
userAgent: navigator.userAgent,
|
||||
});
|
||||
|
||||
// Manually request query string execute as the card will be
|
||||
|
||||
@@ -17,25 +17,25 @@ import {
|
||||
ViewDisplayMode,
|
||||
} from '../config/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { FrigateCardError } from '../types';
|
||||
import { FrigateCardError, MediaLoadedInfo } from '../types';
|
||||
import { desparsifyArrays } from '../utils/basic';
|
||||
import { isCompanionApp } from '../utils/companion';
|
||||
import { CardConditionAPI, KeysState, MicrophoneState } from './types';
|
||||
|
||||
interface ConditionState {
|
||||
view?: string;
|
||||
fullscreen?: boolean;
|
||||
expand?: boolean;
|
||||
export interface ConditionState {
|
||||
camera?: string;
|
||||
state?: HassEntities;
|
||||
media_loaded?: boolean;
|
||||
displayMode?: ViewDisplayMode;
|
||||
triggered?: Set<string>;
|
||||
expand?: boolean;
|
||||
fullscreen?: boolean;
|
||||
interaction?: boolean;
|
||||
microphone?: MicrophoneState;
|
||||
user?: CurrentUser;
|
||||
keys?: KeysState;
|
||||
user_agent?: string;
|
||||
mediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
microphone?: MicrophoneState;
|
||||
state?: HassEntities;
|
||||
triggered?: Set<string>;
|
||||
user?: CurrentUser;
|
||||
userAgent?: string;
|
||||
view?: string;
|
||||
}
|
||||
|
||||
class OverrideConfigurationError extends FrigateCardError {}
|
||||
@@ -152,7 +152,10 @@ export interface ConditionsManagerEpoch {
|
||||
manager: Readonly<ConditionsManager>;
|
||||
}
|
||||
|
||||
export type ConditionsManagerListener = () => void;
|
||||
export type ConditionsManagerListener = (
|
||||
newState: ConditionState,
|
||||
oldState?: ConditionState,
|
||||
) => void;
|
||||
|
||||
export class ConditionsManager {
|
||||
protected _api: CardConditionAPI;
|
||||
@@ -166,7 +169,7 @@ export class ConditionsManager {
|
||||
// the configuration needs to consume it.
|
||||
protected _hasHAStateConditions = false;
|
||||
protected _mediaQueries: MediaQueryList[] = [];
|
||||
protected _mediaQueryTrigger = () => this._triggerChange();
|
||||
protected _mediaQueryTrigger = () => this._triggerChange(this._state);
|
||||
|
||||
constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) {
|
||||
this._api = api;
|
||||
@@ -177,6 +180,14 @@ export class ConditionsManager {
|
||||
];
|
||||
}
|
||||
|
||||
public addListener(listener: ConditionsManagerListener): void {
|
||||
this._listeners.push(listener);
|
||||
}
|
||||
|
||||
public removeListener(listener: ConditionsManagerListener): void {
|
||||
this._listeners = this._listeners.filter((l) => l !== listener);
|
||||
}
|
||||
|
||||
public removeConditions(): void {
|
||||
this._mediaQueries.forEach((mql) =>
|
||||
mql.removeEventListener('change', this._mediaQueryTrigger),
|
||||
@@ -233,12 +244,12 @@ export class ConditionsManager {
|
||||
if (Object.keys(state).every((key) => isEqual(state[key], this._state[key]))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldState = this._state;
|
||||
this._state = {
|
||||
...this._state,
|
||||
...oldState,
|
||||
...state,
|
||||
};
|
||||
this._triggerChange();
|
||||
this._triggerChange(oldState);
|
||||
}
|
||||
|
||||
public getState(): ConditionState {
|
||||
@@ -313,8 +324,8 @@ export class ConditionsManager {
|
||||
return !!state.user && conditionObj.users.includes(state.user.id);
|
||||
case 'media_loaded':
|
||||
return (
|
||||
state.media_loaded !== undefined &&
|
||||
conditionObj.media_loaded === state.media_loaded
|
||||
state.mediaLoadedInfo !== undefined &&
|
||||
conditionObj.media_loaded === !!state.mediaLoadedInfo
|
||||
);
|
||||
case 'screen':
|
||||
return window.matchMedia(conditionObj.media_query).matches;
|
||||
@@ -352,12 +363,12 @@ export class ConditionsManager {
|
||||
);
|
||||
case 'user_agent':
|
||||
return (
|
||||
!!state.user_agent &&
|
||||
(!conditionObj.user_agent || conditionObj.user_agent === state.user_agent) &&
|
||||
!!state.userAgent &&
|
||||
(!conditionObj.user_agent || conditionObj.user_agent === state.userAgent) &&
|
||||
(conditionObj.companion === undefined ||
|
||||
conditionObj.companion === isCompanionApp(state.user_agent)) &&
|
||||
conditionObj.companion === isCompanionApp(state.userAgent)) &&
|
||||
(conditionObj.user_agent_re === undefined ||
|
||||
new RegExp(conditionObj.user_agent_re).test(state.user_agent))
|
||||
new RegExp(conditionObj.user_agent_re).test(state.userAgent))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -366,8 +377,8 @@ export class ConditionsManager {
|
||||
return { manager: this };
|
||||
}
|
||||
|
||||
protected _triggerChange(): void {
|
||||
protected _triggerChange(oldState?: ConditionState): void {
|
||||
this._epoch = this._createEpoch();
|
||||
this._listeners.forEach((listener) => listener());
|
||||
this._listeners.forEach((listener) => listener(this._state, oldState));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import { ConfigManager } from './config/config-manager';
|
||||
import { DefaultManager } from './default-manager';
|
||||
import { DownloadManager } from './download-manager';
|
||||
import { ExpandManager } from './expand-manager';
|
||||
import { FullscreenManager } from './fullscreen-manager';
|
||||
import { FullscreenManager } from './fullscreen/fullscreen-manager';
|
||||
import { HASSManager } from './hass/hass-manager';
|
||||
import { InitializationManager } from './initialization-manager';
|
||||
import { InteractionManager } from './interaction-manager';
|
||||
|
||||
@@ -23,7 +23,7 @@ export class ExpandManager {
|
||||
public setExpanded(expanded: boolean): void {
|
||||
if (expanded && this._api.getFullscreenManager().isInFullscreen()) {
|
||||
// Fullscreen and expanded mode are mutually exclusive.
|
||||
this._api.getFullscreenManager().stopFullscreen();
|
||||
this._api.getFullscreenManager().setFullscreen(false);
|
||||
}
|
||||
|
||||
this._expanded = expanded;
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { CardFullscreenAPI } from './types';
|
||||
|
||||
export class FullscreenManager {
|
||||
protected _api: CardFullscreenAPI;
|
||||
|
||||
constructor(api: CardFullscreenAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this._setConditionState();
|
||||
}
|
||||
|
||||
public connect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on('change', this._fullscreenHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.off('change', this._fullscreenHandler);
|
||||
}
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return screenfull.isEnabled && screenfull.isFullscreen;
|
||||
}
|
||||
|
||||
public toggleFullscreen(): void {
|
||||
screenfull.toggle(this._api.getCardElementManager().getElement());
|
||||
}
|
||||
|
||||
public stopFullscreen(): void {
|
||||
screenfull.exit();
|
||||
}
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager()?.setState({
|
||||
fullscreen: this.isInFullscreen(),
|
||||
});
|
||||
}
|
||||
|
||||
protected _fullscreenHandler = (): void => {
|
||||
this._api.getExpandManager().setExpanded(false);
|
||||
|
||||
this._setConditionState();
|
||||
|
||||
// Re-render after a change to fullscreen mode to take advantage of
|
||||
// the expanded screen real-estate (vs staying in aspect-ratio locked
|
||||
// modes).
|
||||
this._api.getCardElementManager().update();
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { WebkitHTMLVideoElement } from '../../types';
|
||||
import { CardFullscreenAPI } from '../types';
|
||||
import { ScreenfullFullScreenProvider } from './screenfull';
|
||||
import { FullscreenHandler, FullscreenProvider } from './types';
|
||||
import { WebkitFullScreenProvider } from './webkit';
|
||||
|
||||
export class FullscreenProviderFactory {
|
||||
public static create(
|
||||
api: CardFullscreenAPI,
|
||||
handler: FullscreenHandler,
|
||||
): FullscreenProvider | null {
|
||||
if (screenfull.isEnabled) {
|
||||
return new ScreenfullFullScreenProvider(api, handler);
|
||||
}
|
||||
|
||||
const video = document.createElement('video') as Partial<WebkitHTMLVideoElement>;
|
||||
if (!!video.webkitEnterFullscreen) {
|
||||
return new WebkitFullScreenProvider(api, handler);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { isBeingCasted } from '../../utils/casting';
|
||||
import { CardFullscreenAPI } from '../types';
|
||||
import { FullscreenProviderFactory } from './factory';
|
||||
import { FullscreenProvider } from './types';
|
||||
|
||||
export class FullscreenManager {
|
||||
protected _api: CardFullscreenAPI;
|
||||
protected _provider: FullscreenProvider | null;
|
||||
|
||||
constructor(api: CardFullscreenAPI, provider?: FullscreenProvider) {
|
||||
this._api = api;
|
||||
this._provider =
|
||||
provider ?? FullscreenProviderFactory.create(api, this._fullscreenHandler);
|
||||
}
|
||||
|
||||
public connect(): void {
|
||||
this._provider?.connect();
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this._provider?.disconnect();
|
||||
}
|
||||
|
||||
public setFullscreen(fullscreen: boolean): void {
|
||||
this._provider?.setFullscreen(fullscreen);
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return this._provider?.isInFullscreen() ?? false;
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
if (isBeingCasted()) {
|
||||
return false;
|
||||
}
|
||||
return this._provider?.isSupported() ?? false;
|
||||
}
|
||||
|
||||
public initialize(): void {
|
||||
this._setConditionState();
|
||||
}
|
||||
|
||||
public toggleFullscreen(): void {
|
||||
if (this._provider?.isInFullscreen()) {
|
||||
this._provider.setFullscreen(false);
|
||||
} else {
|
||||
this._provider?.setFullscreen(true);
|
||||
}
|
||||
}
|
||||
|
||||
protected _fullscreenHandler = (): void => {
|
||||
this._api.getExpandManager().setExpanded(false);
|
||||
|
||||
this._setConditionState();
|
||||
|
||||
// Re-render after a change to fullscreen mode to take advantage of
|
||||
// the expanded screen real-estate (vs staying in aspect-ratio locked
|
||||
// modes).
|
||||
this._api.getCardElementManager().update();
|
||||
};
|
||||
|
||||
protected _setConditionState(): void {
|
||||
this._api.getConditionsManager()?.setState({
|
||||
fullscreen: this.isInFullscreen(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CardFullscreenAPI } from '../types';
|
||||
import { FullscreenHandler } from './types';
|
||||
|
||||
export class FullscreenProviderBase {
|
||||
protected _api: CardFullscreenAPI;
|
||||
protected _handler: FullscreenHandler;
|
||||
|
||||
constructor(api: CardFullscreenAPI, handler: FullscreenHandler) {
|
||||
this._api = api;
|
||||
this._handler = handler;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import screenfull from 'screenfull';
|
||||
import { FullscreenProviderBase } from '../provider';
|
||||
import { FullscreenProvider } from '../types';
|
||||
|
||||
export class ScreenfullFullScreenProvider
|
||||
extends FullscreenProviderBase
|
||||
implements FullscreenProvider
|
||||
{
|
||||
public connect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.on('change', this._handler);
|
||||
}
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
if (screenfull.isEnabled) {
|
||||
screenfull.off('change', this._handler);
|
||||
}
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return screenfull.isEnabled && screenfull.isFullscreen;
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
return screenfull.isEnabled;
|
||||
}
|
||||
|
||||
public setFullscreen(fullscreen: boolean): void {
|
||||
if (!this.isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (fullscreen) {
|
||||
screenfull.request(this._api.getCardElementManager().getElement());
|
||||
} else {
|
||||
screenfull.exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export type FullscreenHandler = () => void;
|
||||
|
||||
export interface FullscreenProvider {
|
||||
connect(): void;
|
||||
disconnect(): void;
|
||||
|
||||
isInFullscreen(): boolean;
|
||||
isSupported(): boolean;
|
||||
|
||||
setFullscreen(fullscreen: boolean): void;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { WebkitHTMLVideoElement } from '../../../types';
|
||||
import { Timer } from '../../../utils/timer';
|
||||
import { ConditionState } from '../../conditions-manager';
|
||||
import { FullscreenProviderBase } from '../provider';
|
||||
import { FullscreenProvider } from '../types';
|
||||
|
||||
const WEBKIT_PLAY_SECONDS = 0.5;
|
||||
|
||||
/**
|
||||
* Fullscreen implementation for webkit based browsers that do not support the
|
||||
* standard Fullscreen API (expected to be exclusively iOS on an iPhone).
|
||||
* See: https://github.com/dermotduffy/frigate-hass-card/issues/1444
|
||||
*/
|
||||
export class WebkitFullScreenProvider
|
||||
extends FullscreenProviderBase
|
||||
implements FullscreenProvider
|
||||
{
|
||||
protected _playTimer = new Timer();
|
||||
|
||||
public connect(): void {
|
||||
this._api.getConditionsManager().addListener(this._conditionChangeHandler);
|
||||
}
|
||||
|
||||
public disconnect(): void {
|
||||
this._api.getConditionsManager().removeListener(this._conditionChangeHandler);
|
||||
}
|
||||
|
||||
protected _conditionChangeHandler = (
|
||||
newState: ConditionState,
|
||||
oldState?: ConditionState,
|
||||
): void => {
|
||||
if (
|
||||
newState.mediaLoadedInfo?.player?.getFullscreenElement() !==
|
||||
oldState?.mediaLoadedInfo?.player?.getFullscreenElement()
|
||||
) {
|
||||
const oldElement = oldState?.mediaLoadedInfo?.player?.getFullscreenElement();
|
||||
oldElement?.removeEventListener('webkitbeginfullscreen', this._handler);
|
||||
oldElement?.removeEventListener('webkitendfullscreen', this._endHandler);
|
||||
|
||||
const newElement = newState.mediaLoadedInfo?.player?.getFullscreenElement();
|
||||
newElement?.addEventListener('webkitbeginfullscreen', this._handler);
|
||||
newElement?.addEventListener('webkitendfullscreen', this._endHandler);
|
||||
}
|
||||
};
|
||||
|
||||
protected _getVideoElement():
|
||||
| (HTMLVideoElement & Partial<WebkitHTMLVideoElement>)
|
||||
| null {
|
||||
const element = this._api
|
||||
.getMediaLoadedInfoManager()
|
||||
.get()
|
||||
?.player?.getFullscreenElement();
|
||||
return element instanceof HTMLVideoElement ? element : null;
|
||||
}
|
||||
|
||||
public isInFullscreen(): boolean {
|
||||
return !!this._getVideoElement()?.webkitDisplayingFullscreen;
|
||||
}
|
||||
|
||||
public isSupported(): boolean {
|
||||
return !!this._getVideoElement()?.webkitSupportsFullscreen;
|
||||
}
|
||||
|
||||
public setFullscreen(fullscreen: boolean): void {
|
||||
if (!this.isSupported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const video = this._getVideoElement();
|
||||
|
||||
if (fullscreen) {
|
||||
video?.webkitEnterFullscreen?.();
|
||||
} else {
|
||||
video?.webkitExitFullscreen?.();
|
||||
}
|
||||
}
|
||||
|
||||
protected _endHandler = (): void => {
|
||||
this._handler();
|
||||
|
||||
// Webkit on iPhone stops the video when exiting fullscreen (why!). This
|
||||
// hack workaround has the disadvantage that there's no way to tell if the
|
||||
// video was legitimately paused before they user exited fullscreen (as the
|
||||
// common path is for exit fullscreen to not involve
|
||||
// setFullscreen(...) being called at all, but rather the user simply
|
||||
// clicking the 'X' which then fires this event). That's probably the rare
|
||||
// case though.
|
||||
this._playTimer.start(WEBKIT_PLAY_SECONDS, () => {
|
||||
this._getVideoElement()?.play();
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -16,21 +16,21 @@ export class MediaLoadedInfoManager {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public set(mediaInfo: MediaLoadedInfo): void {
|
||||
if (!isValidMediaLoadedInfo(mediaInfo)) {
|
||||
public set(mediaLoadedInfo: MediaLoadedInfo): void {
|
||||
if (!isValidMediaLoadedInfo(mediaLoadedInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log(
|
||||
this._api.getConfigManager().getCardWideConfig(),
|
||||
`Frigate Card media load: `,
|
||||
mediaInfo,
|
||||
mediaLoadedInfo,
|
||||
);
|
||||
|
||||
this._current = mediaInfo;
|
||||
this._lastKnown = mediaInfo;
|
||||
this._current = mediaLoadedInfo;
|
||||
this._lastKnown = mediaLoadedInfo;
|
||||
|
||||
this._api.getConditionsManager().setState({ media_loaded: true });
|
||||
this._api.getConditionsManager().setState({ mediaLoadedInfo: mediaLoadedInfo });
|
||||
|
||||
// Fresh media information may change how the card is rendered.
|
||||
this._api.getStyleManager().setExpandedMode();
|
||||
@@ -47,7 +47,7 @@ export class MediaLoadedInfoManager {
|
||||
|
||||
public clear(): void {
|
||||
this._current = null;
|
||||
this._api.getConditionsManager().setState({ media_loaded: false });
|
||||
this._api.getConditionsManager().setState({ mediaLoadedInfo: null });
|
||||
}
|
||||
|
||||
public has(): boolean {
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { ConfigManager } from './config/config-manager';
|
||||
import type { DefaultManager } from './default-manager';
|
||||
import type { DownloadManager } from './download-manager';
|
||||
import type { ExpandManager } from './expand-manager';
|
||||
import type { FullscreenManager } from './fullscreen-manager';
|
||||
import type { FullscreenManager } from './fullscreen/fullscreen-manager';
|
||||
import type { HASSManager } from './hass/hass-manager';
|
||||
import type { InitializationManager } from './initialization-manager';
|
||||
import type { InteractionManager } from './interaction-manager';
|
||||
@@ -150,6 +150,7 @@ export interface CardFullscreenAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getExpandManager(): ExpandManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
getMediaPlayerManager(): MediaPlayerManager;
|
||||
}
|
||||
|
||||
@@ -205,6 +206,7 @@ export interface CardMediaLoadedAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionsManager(): ConditionsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getFullscreenManager(): FullscreenManager;
|
||||
getStyleManager(): StyleManager;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user