@@ -10,7 +10,7 @@ import {
|
||||
getActionConfigGivenAction,
|
||||
isAdvancedCameraCardCustomAction,
|
||||
} from '../../utils/action.js';
|
||||
import { allPromises } from '../../utils/basic.js';
|
||||
import { allPromises, errorToConsole } from '../../utils/basic.js';
|
||||
import { TemplateRenderer } from '../templates/index.js';
|
||||
import { CardActionsManagerAPI } from '../types.js';
|
||||
import { ActionSet } from './actions/set.js';
|
||||
@@ -53,7 +53,7 @@ export class ActionsManager implements ActionsExecutor {
|
||||
let specificActions: Actions | undefined = undefined;
|
||||
if (view?.is('live')) {
|
||||
specificActions = config?.live.actions;
|
||||
} else if (view?.isGalleryView()) {
|
||||
} else if (view?.isMediaGalleryView()) {
|
||||
specificActions = config?.media_gallery?.actions;
|
||||
} else if (view?.isViewerView()) {
|
||||
specificActions = config?.media_viewer.actions;
|
||||
@@ -150,6 +150,7 @@ export class ActionsManager implements ActionsExecutor {
|
||||
await actionSet.execute(this._api);
|
||||
forwardHaptic('success');
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
forwardHaptic('warning');
|
||||
}
|
||||
this._actionsInFlight = this._actionsInFlight.filter((a) => a !== actionSet);
|
||||
|
||||
@@ -6,6 +6,9 @@ export class DownloadAction extends AdvancedCameraCardAction<GeneralActionConfig
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
await api.getDownloadManager().downloadViewerMedia();
|
||||
const item = api.getViewManager().getView()?.queryResults?.getSelectedResult();
|
||||
if (item) {
|
||||
await api.getViewItemManager().download(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { FolderActionConfig } from '../../../config/schema/actions/custom/folder';
|
||||
import { FolderViewQuery } from '../../../view/query';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class FolderAction extends AdvancedCameraCardAction<FolderActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const folder = api.getFoldersManager().getFolder(this._action.folder);
|
||||
if (!folder) {
|
||||
return;
|
||||
}
|
||||
|
||||
const query = api.getFoldersManager().generateDefaultFolderQuery(folder);
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.getViewManager().setViewByParametersWithExistingQuery({
|
||||
params: {
|
||||
view: 'folder',
|
||||
query: new FolderViewQuery(query),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { MediaPlayerActionConfig } from '../../../config/schema/actions/custom/media-player';
|
||||
import { getStreamCameraID } from '../../../utils/substream';
|
||||
import { ViewItemClassifier } from '../../../view/item-classifier';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
@@ -9,15 +10,19 @@ export class MediaPlayerAction extends AdvancedCameraCardAction<MediaPlayerActio
|
||||
|
||||
const mediaPlayer = this._action.media_player;
|
||||
const mediaPlayerController = api.getMediaPlayerManager();
|
||||
const view = api.getViewManager().getView();
|
||||
const media = view?.queryResults?.getSelectedResult() ?? null;
|
||||
|
||||
if (this._action.media_player_action === 'stop') {
|
||||
await mediaPlayerController.stop(mediaPlayer);
|
||||
} else if (view?.is('live')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const view = api.getViewManager().getView();
|
||||
const item = view?.queryResults?.getSelectedResult() ?? null;
|
||||
|
||||
if (view?.is('live')) {
|
||||
await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view));
|
||||
} else if (view?.isViewerView() && media) {
|
||||
await mediaPlayerController.playMedia(mediaPlayer, media);
|
||||
} else if (view?.isViewerView() && item && ViewItemClassifier.isMedia(item)) {
|
||||
await mediaPlayerController.playMedia(mediaPlayer, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import clamp from 'lodash-es/clamp';
|
||||
import { clamp } from 'lodash-es';
|
||||
import {
|
||||
PartialZoomSettings,
|
||||
ZOOM_DEFAULT_PAN_X,
|
||||
|
||||
@@ -86,12 +86,15 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||
setInProgressForThisTarget(ptzCameraID, this._context, 'ptz', this);
|
||||
|
||||
const singleStep = async (): Promise<void> => {
|
||||
this._action.ptz_action &&
|
||||
(await api
|
||||
/* istanbul ignore else: the else path cannot be reached as ptz_action
|
||||
being present is checked above -- @preserve */
|
||||
if (this._action.ptz_action) {
|
||||
await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (!this._stopped) {
|
||||
// Only start the timer for the next step after this step returns, and
|
||||
@@ -121,13 +124,16 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||
});
|
||||
|
||||
this._timer.start(ptzConfiguration.c2r_delay_between_calls_seconds, async () => {
|
||||
this._action.ptz_action &&
|
||||
(await api
|
||||
/* istanbul ignore else: the else path cannot be reached as ptz_action
|
||||
being present is checked above -- @preserve */
|
||||
if (this._action.ptz_action) {
|
||||
await api
|
||||
.getCameraManager()
|
||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||
preset: this._action.ptz_preset,
|
||||
phase: 'stop',
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { GeneralActionConfig } from '../../../config/schema/actions/custom/general';
|
||||
import { downloadURL } from '../../../utils/download';
|
||||
import { generateScreenshotFilename } from '../../../utils/screenshot';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
@@ -6,6 +8,13 @@ export class ScreenshotAction extends AdvancedCameraCardAction<GeneralActionConf
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
await api.getDownloadManager().downloadScreenshot();
|
||||
const url = await api
|
||||
.getMediaLoadedInfoManager()
|
||||
.get()
|
||||
?.mediaPlayerController?.getScreenshotURL();
|
||||
|
||||
if (url) {
|
||||
downloadURL(url, generateScreenshotFilename(api.getViewManager().getView()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SleepActionConfig } from '../../../config/schema/actions/custom/sleep';
|
||||
import { sleep } from '../../../utils/basic';
|
||||
import { sleep } from '../../../utils/sleep';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { timeDeltaToSeconds } from '../utils/time-delta';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
@@ -3,7 +3,6 @@ import { CardActionsAPI } from '../../types';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class StatusBarAction extends AdvancedCameraCardAction<StatusBarActionConfig> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { DefaultAction } from './actions/default';
|
||||
import { DisplayModeSelectAction } from './actions/display-mode-select';
|
||||
import { DownloadAction } from './actions/download';
|
||||
import { ExpandAction } from './actions/expand';
|
||||
import { FolderAction } from './actions/folder';
|
||||
import { FullscreenAction } from './actions/fullscreen';
|
||||
import { InternalCallbackAction } from './actions/internal-callback';
|
||||
import { LogAction } from './actions/log';
|
||||
@@ -150,6 +151,8 @@ export class ActionFactory {
|
||||
return new StatusBarAction(context, action, options?.config);
|
||||
case INTERNAL_CALLBACK_ACTION:
|
||||
return new InternalCallbackAction(context, action, options?.config);
|
||||
case 'folder':
|
||||
return new FolderAction(context, action, options?.config);
|
||||
}
|
||||
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import merge from 'lodash-es/merge';
|
||||
import { merge } from 'lodash-es';
|
||||
import { Action, TargetedActionContext } from '../types';
|
||||
import { ActionContext } from 'action';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ViewItemClassifier } from '../view/item-classifier';
|
||||
import { CardCameraURLAPI } from './types';
|
||||
|
||||
export class CameraURLManager {
|
||||
@@ -20,11 +21,11 @@ export class CameraURLManager {
|
||||
|
||||
public getCameraURL(): string | null {
|
||||
const view = this._api.getViewManager().getView();
|
||||
const media = view?.queryResults?.getSelectedResult() ?? null;
|
||||
const item = view?.queryResults?.getSelectedResult() ?? null;
|
||||
const endpoints = view?.camera
|
||||
? this._api.getCameraManager().getCameraEndpoints(view.camera, {
|
||||
view: view.view,
|
||||
...(media && { media: media }),
|
||||
...(item && ViewItemClassifier.isMedia(item) && { media: item }),
|
||||
}) ?? null
|
||||
: null;
|
||||
return endpoints?.ui?.endpoint ?? null;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LitElement, ReactiveControllerHost } from 'lit';
|
||||
import { ActionEventTarget } from '../action-handler-directive';
|
||||
import { isCardInPanel } from '../ha/panel';
|
||||
import { setOrRemoveAttribute } from '../utils/basic';
|
||||
import { isBeingCasted } from '../utils/casting';
|
||||
import { isCardInPanel } from '../utils/ha';
|
||||
import { ActionExecutionRequestEventTarget } from './actions/utils/execution-request';
|
||||
import { InitializationAspect } from './initialization-manager';
|
||||
import { CardElementAPI } from './types';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { ConditionsManager } from '../../conditions/conditions-manager.js';
|
||||
import { isConfigUpgradeable } from '../../config/management.js';
|
||||
import { setProfiles } from '../../config/profiles/set-profiles.js';
|
||||
@@ -15,6 +15,7 @@ import { CardConfigAPI } from '../types.js';
|
||||
import { getOverriddenConfig } from './get-overridden-config.js';
|
||||
import { setAutomationsFromConfig } from './load-automations.js';
|
||||
import { setRemoteControlEntityFromConfig } from './load-control-entities.js';
|
||||
import { setFoldersFromConfig } from './load-folders.js';
|
||||
import { setKeyboardShortcutsFromConfig } from './load-keyboard-shortcuts.js';
|
||||
|
||||
export class ConfigManager {
|
||||
@@ -136,6 +137,7 @@ export class ConfigManager {
|
||||
const previousConfig = this._overriddenConfig;
|
||||
this._overriddenConfig = overriddenConfig;
|
||||
|
||||
setFoldersFromConfig(this._api);
|
||||
this._api.getStyleManager().updateFromConfig();
|
||||
|
||||
if (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CardConfigLoaderAPI } from '../types';
|
||||
|
||||
export const setAutomationsFromConfig = (api: CardConfigLoaderAPI) => {
|
||||
export const setAutomationsFromConfig = (api: CardConfigLoaderAPI): void => {
|
||||
api.getAutomationsManager().deleteAutomations();
|
||||
api
|
||||
.getAutomationsManager()
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { CardConfigLoaderAPI } from '../types';
|
||||
|
||||
export const setFoldersFromConfig = (api: CardConfigLoaderAPI): void => {
|
||||
api.getFoldersManager().deleteFolders();
|
||||
try {
|
||||
api
|
||||
.getFoldersManager()
|
||||
.addFolders(api.getConfigManager().getConfig()?.folders ?? []);
|
||||
} catch (ev) {
|
||||
api.getMessageManager().setErrorIfHigherPriority(ev);
|
||||
}
|
||||
};
|
||||
@@ -2,17 +2,12 @@ import { ReactiveController } from 'lit';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { ConditionStateManager } from '../conditions/state-manager';
|
||||
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
||||
import { DeviceRegistryManager } from '../ha/registry/device';
|
||||
import { DeviceCache } from '../ha/registry/device/types';
|
||||
import { EntityRegistryManagerLive } from '../ha/registry/entity';
|
||||
import { EntityCache, EntityRegistryManager } from '../ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../ha/resolved-media';
|
||||
import { LovelaceCardEditor } from '../ha/types';
|
||||
import {
|
||||
createDeviceRegistryCache,
|
||||
DeviceRegistryManager,
|
||||
} from '../utils/ha/registry/device';
|
||||
import {
|
||||
createEntityRegistryCache,
|
||||
EntityRegistryManagerLive,
|
||||
} from '../utils/ha/registry/entity';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import { ActionsManager } from './actions/actions-manager';
|
||||
import { AutomationsManager } from './automations-manager';
|
||||
import { CameraURLManager } from './camera-url-manager';
|
||||
@@ -24,8 +19,8 @@ import {
|
||||
} from './card-element-manager';
|
||||
import { ConfigManager } from './config/config-manager';
|
||||
import { DefaultManager } from './default-manager';
|
||||
import { DownloadManager } from './download-manager';
|
||||
import { ExpandManager } from './expand-manager';
|
||||
import { FoldersManager } from './folders/manager';
|
||||
import { FullscreenManager } from './fullscreen/fullscreen-manager';
|
||||
import { HASSManager } from './hass/hass-manager';
|
||||
import { InitializationManager } from './initialization-manager';
|
||||
@@ -65,6 +60,7 @@ import {
|
||||
CardTriggersAPI,
|
||||
CardViewAPI,
|
||||
} from './types';
|
||||
import { ViewItemManager } from './view/item-manager';
|
||||
import { ViewManager } from './view/view-manager';
|
||||
|
||||
export class CardController
|
||||
@@ -98,12 +94,8 @@ export class CardController
|
||||
|
||||
// These properties may be used in the construction of 'managers' (and should
|
||||
// be created first).
|
||||
protected _deviceRegistryManager = new DeviceRegistryManager(
|
||||
createDeviceRegistryCache(),
|
||||
);
|
||||
protected _entityRegistryManager = new EntityRegistryManagerLive(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
protected _deviceRegistryManager = new DeviceRegistryManager(new DeviceCache());
|
||||
protected _entityRegistryManager = new EntityRegistryManagerLive(new EntityCache());
|
||||
protected _resolvedMediaCache = new ResolvedMediaCache();
|
||||
|
||||
protected _actionsManager = new ActionsManager(this, new TemplateRenderer());
|
||||
@@ -113,8 +105,8 @@ export class CardController
|
||||
protected _cardElementManager: CardElementManager;
|
||||
protected _configManager = new ConfigManager(this);
|
||||
protected _defaultManager = new DefaultManager(this);
|
||||
protected _downloadManager = new DownloadManager(this);
|
||||
protected _expandManager = new ExpandManager(this);
|
||||
protected _foldersManager = new FoldersManager(this);
|
||||
protected _fullscreenManager = new FullscreenManager(this);
|
||||
protected _hassManager = new HASSManager(this);
|
||||
protected _initializationManager = new InitializationManager(this);
|
||||
@@ -129,6 +121,7 @@ export class CardController
|
||||
protected _styleManager = new StyleManager(this);
|
||||
protected _triggersManager = new TriggersManager(this);
|
||||
protected _viewManager = new ViewManager(this);
|
||||
protected _viewItemManager = new ViewItemManager(this);
|
||||
|
||||
constructor(
|
||||
host: CardHTMLElement,
|
||||
@@ -193,10 +186,6 @@ export class CardController
|
||||
return this._deviceRegistryManager;
|
||||
}
|
||||
|
||||
public getDownloadManager(): DownloadManager {
|
||||
return this._downloadManager;
|
||||
}
|
||||
|
||||
public getEntityRegistryManager(): EntityRegistryManager {
|
||||
return this._entityRegistryManager;
|
||||
}
|
||||
@@ -205,6 +194,10 @@ export class CardController
|
||||
return this._expandManager;
|
||||
}
|
||||
|
||||
public getFoldersManager(): FoldersManager {
|
||||
return this._foldersManager;
|
||||
}
|
||||
|
||||
public getFullscreenManager(): FullscreenManager {
|
||||
return this._fullscreenManager;
|
||||
}
|
||||
@@ -282,6 +275,10 @@ export class CardController
|
||||
return this._viewManager;
|
||||
}
|
||||
|
||||
public getViewItemManager(): ViewItemManager {
|
||||
return this._viewItemManager;
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
// Handlers
|
||||
// *************************************************************************
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
||||
import { createGeneralAction } from '../utils/action';
|
||||
import { isActionAllowedBasedOnInteractionState } from '../utils/interaction-mode';
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { downloadMedia, downloadURL } from '../utils/download';
|
||||
import { generateScreenshotFilename } from '../utils/screenshot';
|
||||
import { CardDownloadAPI } from './types';
|
||||
|
||||
export class DownloadManager {
|
||||
protected _api: CardDownloadAPI;
|
||||
|
||||
constructor(api: CardDownloadAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public async downloadViewerMedia(): Promise<boolean> {
|
||||
const media = this._api
|
||||
.getViewManager()
|
||||
.getView()
|
||||
?.queryResults?.getSelectedResult();
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
|
||||
if (!media || !hass) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await downloadMedia(hass, this._api.getCameraManager(), media);
|
||||
} catch (error: unknown) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async downloadScreenshot(): Promise<void> {
|
||||
const url = await this._api
|
||||
.getMediaLoadedInfoManager()
|
||||
.get()
|
||||
?.mediaPlayerController?.getScreenshotURL();
|
||||
if (url) {
|
||||
downloadURL(url, generateScreenshotFilename(this._api.getViewManager().getView()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { FolderConfig, FolderType, folderTypeSchema } from '../../config/schema/folders';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Endpoint } from '../../types';
|
||||
import { ViewItem } from '../../view/item';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
import { sortItems } from '../view/sort';
|
||||
import { HAFoldersEngine } from './ha/engine';
|
||||
import { DownloadHelpers, EngineOptions, FolderQuery, FoldersEngine } from './types';
|
||||
|
||||
export class FoldersExecutor {
|
||||
private _ha: FoldersEngine;
|
||||
|
||||
constructor(engines?: { ha?: HAFoldersEngine }) {
|
||||
this._ha = engines?.ha ?? new HAFoldersEngine();
|
||||
}
|
||||
|
||||
public generateDefaultFolderQuery(folder: FolderConfig): FolderQuery | null {
|
||||
return (
|
||||
this._getFolderEngine(folder.type)?.generateDefaultFolderQuery(folder) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public async expandFolder(
|
||||
hass: HomeAssistant,
|
||||
query: FolderQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<ViewItem[] | null> {
|
||||
const results =
|
||||
(await this._getFolderEngine(query.folder.type)?.expandFolder(
|
||||
hass,
|
||||
query,
|
||||
engineOptions,
|
||||
)) ?? null;
|
||||
return results ? sortItems(results) : null;
|
||||
}
|
||||
|
||||
public getItemCapabilities(item: ViewItem): ViewItemCapabilities | null {
|
||||
return (
|
||||
this._getFolderEngine(item.getFolder()?.type)?.getItemCapabilities(item) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
public async getDownloadPath(
|
||||
hass: HomeAssistant | null,
|
||||
item: ViewItem,
|
||||
helpers?: DownloadHelpers,
|
||||
): Promise<Endpoint | null> {
|
||||
return await (this._getFolderEngine(item.getFolder()?.type)?.getDownloadPath(
|
||||
hass,
|
||||
item,
|
||||
helpers,
|
||||
) ?? null);
|
||||
}
|
||||
|
||||
public async favorite(
|
||||
hass: HomeAssistant | null,
|
||||
item: ViewItem,
|
||||
favorite: boolean,
|
||||
): Promise<void> {
|
||||
return await this._getFolderEngine(item.getFolder()?.type)?.favorite(
|
||||
hass,
|
||||
item,
|
||||
favorite,
|
||||
);
|
||||
}
|
||||
|
||||
private _getFolderEngine(type?: FolderType): FoldersEngine | null {
|
||||
switch (type) {
|
||||
case folderTypeSchema.enum.ha:
|
||||
return this._ha;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { NonEmptyTuple } from 'type-fest';
|
||||
import {
|
||||
FolderConfig,
|
||||
folderTypeSchema,
|
||||
HA_MEDIA_SOURCE_ROOT,
|
||||
HAFolderConfig,
|
||||
HAFolderPathComponent,
|
||||
} from '../../../config/schema/folders';
|
||||
import { getViewItemsFromBrowseMediaArray } from '../../../ha/browse-media/browse-media-to-view-media';
|
||||
import { BrowseMedia, BrowseMediaCache } from '../../../ha/browse-media/types';
|
||||
import {
|
||||
BrowseMediaStep,
|
||||
BrowseMediaTarget,
|
||||
BrowseMediaWalker,
|
||||
} from '../../../ha/browse-media/walker';
|
||||
import { getMediaDownloadPath } from '../../../ha/download';
|
||||
import { HomeAssistant } from '../../../ha/types';
|
||||
import { Endpoint } from '../../../types';
|
||||
import { ViewItem } from '../../../view/item';
|
||||
import { ViewItemClassifier } from '../../../view/item-classifier';
|
||||
import { ViewItemCapabilities } from '../../../view/types';
|
||||
import {
|
||||
DownloadHelpers,
|
||||
EngineOptions,
|
||||
FolderPathComponent,
|
||||
FolderQuery,
|
||||
FoldersEngine,
|
||||
} from '../types';
|
||||
|
||||
export class HAFoldersEngine implements FoldersEngine {
|
||||
private _browseMediaManager: BrowseMediaWalker;
|
||||
private _cache = new BrowseMediaCache();
|
||||
|
||||
public constructor(browseMediaManager?: BrowseMediaWalker) {
|
||||
this._browseMediaManager = browseMediaManager ?? new BrowseMediaWalker();
|
||||
}
|
||||
|
||||
public getItemCapabilities(item: ViewItem): ViewItemCapabilities | null {
|
||||
return {
|
||||
canFavorite: false,
|
||||
canDownload: !ViewItemClassifier.isFolder(item),
|
||||
};
|
||||
}
|
||||
|
||||
public async getDownloadPath(
|
||||
hass: HomeAssistant,
|
||||
item: ViewItem,
|
||||
helpers?: DownloadHelpers,
|
||||
): Promise<Endpoint | null> {
|
||||
if (!ViewItemClassifier.isMedia(item)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getMediaDownloadPath(hass, item.getContentID(), helpers?.resolvedMediaCache);
|
||||
}
|
||||
|
||||
public async favorite(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_hass: HomeAssistant,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_item: ViewItem,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_favorite: boolean,
|
||||
): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
public generateDefaultFolderQuery(folder: FolderConfig): FolderQuery | null {
|
||||
if (folder.type !== folderTypeSchema.enum.ha) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
folder,
|
||||
path: this.getDefaultFolderPathComponents(folder.ha),
|
||||
};
|
||||
}
|
||||
|
||||
public async expandFolder(
|
||||
hass: HomeAssistant,
|
||||
query: FolderQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<ViewItem[] | null> {
|
||||
if (query.folder.type !== folderTypeSchema.enum.ha) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pathComponents = [...query.path];
|
||||
|
||||
// Search through the path components from the start to find the last
|
||||
// component with a precise media source id, which is where the queries
|
||||
// start (and may drill down from).
|
||||
let start: string | null = null;
|
||||
while (pathComponents.length > 0) {
|
||||
const id = pathComponents[0]?.id;
|
||||
if (id) {
|
||||
start = id;
|
||||
pathComponents.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no media source id is found, return null, as there is no "starting
|
||||
// query".
|
||||
if (start === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// This matcher matches a browse media against a given path component.
|
||||
const componentMatcher = (
|
||||
media: BrowseMedia,
|
||||
component?: FolderPathComponent,
|
||||
): boolean => {
|
||||
return (
|
||||
!component ||
|
||||
(media.can_expand &&
|
||||
(component.ha?.title === media.title ||
|
||||
(component.ha?.title_re &&
|
||||
new RegExp(component.ha.title_re).test(media.title)) ||
|
||||
component.id === media.media_content_id))
|
||||
);
|
||||
};
|
||||
|
||||
// Generate a walk step, optionally matching against the next path component
|
||||
// (if any), otherwise just returning all the media at this level.
|
||||
const generateStep = (targets: BrowseMediaTarget[]): BrowseMediaStep[] => {
|
||||
const nextComponent = pathComponents.shift();
|
||||
return [
|
||||
{
|
||||
targets,
|
||||
...(nextComponent && {
|
||||
matcher: (media: BrowseMedia) => componentMatcher(media, nextComponent),
|
||||
advance: (targets) => generateStep(targets),
|
||||
}),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
const browseMedia = await this._browseMediaManager.walk(
|
||||
hass,
|
||||
generateStep([start]),
|
||||
{
|
||||
...((engineOptions?.useCache ?? true) && { cache: this._cache }),
|
||||
},
|
||||
);
|
||||
|
||||
return getViewItemsFromBrowseMediaArray(browseMedia, {
|
||||
folder: query.folder,
|
||||
});
|
||||
}
|
||||
|
||||
private getDefaultFolderPathComponents(
|
||||
haFolderConfig?: HAFolderConfig,
|
||||
): NonEmptyTuple<FolderPathComponent> {
|
||||
const shouldAddDefaultRoot = !haFolderConfig?.url && !haFolderConfig?.path?.[0]?.id;
|
||||
|
||||
const defaultPath = [
|
||||
...(shouldAddDefaultRoot ? [{ id: HA_MEDIA_SOURCE_ROOT }] : []),
|
||||
...(haFolderConfig?.url ?? []),
|
||||
...(haFolderConfig?.path ?? []),
|
||||
];
|
||||
|
||||
return defaultPath.map((component) =>
|
||||
this._convertHAPathComponentToFolderPathComponent(component),
|
||||
) as [FolderPathComponent, ...FolderPathComponent[]];
|
||||
}
|
||||
|
||||
// Convert from the HA folder path component config schema to the general,
|
||||
// which pulls `path` to the top level.
|
||||
private _convertHAPathComponentToFolderPathComponent(
|
||||
component: HAFolderPathComponent,
|
||||
): FolderPathComponent {
|
||||
return {
|
||||
id: component.id,
|
||||
ha: {
|
||||
...component,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { FolderConfig } from '../../config/schema/folders';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { Endpoint } from '../../types';
|
||||
import { ViewItem } from '../../view/item';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
import { CardFoldersAPI } from '../types';
|
||||
import { FoldersExecutor } from './executor';
|
||||
import { EngineOptions, FolderInitializationError, FolderQuery } from './types';
|
||||
|
||||
export class FoldersManager {
|
||||
private _api: CardFoldersAPI;
|
||||
private _executor: FoldersExecutor;
|
||||
private _folders: Map<string, FolderConfig> = new Map();
|
||||
|
||||
constructor(api: CardFoldersAPI, executor?: FoldersExecutor) {
|
||||
this._api = api;
|
||||
this._executor = executor ?? new FoldersExecutor();
|
||||
}
|
||||
|
||||
public deleteFolders(): void {
|
||||
this._folders.clear();
|
||||
}
|
||||
|
||||
public addFolders(folders: FolderConfig[]): void {
|
||||
for (const folder of folders) {
|
||||
const folderNumber = this._folders.size;
|
||||
const id = folder.id ?? `folder/${folderNumber.toString()}`;
|
||||
if (this._folders.has(id)) {
|
||||
throw new FolderInitializationError(
|
||||
localize('error.duplicate_folder_id'),
|
||||
folder,
|
||||
);
|
||||
}
|
||||
|
||||
this._folders.set(id, {
|
||||
title: `${localize('common.folder')} ${folderNumber}`,
|
||||
...cloneDeep(folder),
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public getFolderCount(): number {
|
||||
return this._folders.size;
|
||||
}
|
||||
public getFolders(): MapIterator<[string, FolderConfig]> {
|
||||
return this._folders.entries();
|
||||
}
|
||||
public getFolder(id?: string): FolderConfig | null {
|
||||
return id
|
||||
? this._folders.get(id) ?? null
|
||||
: this._folders.values().next().value ?? null;
|
||||
}
|
||||
|
||||
public generateDefaultFolderQuery(folder?: FolderConfig): FolderQuery | null {
|
||||
const _folder = folder ?? this.getFolder();
|
||||
return _folder ? this._executor.generateDefaultFolderQuery(_folder) : null;
|
||||
}
|
||||
|
||||
public async expandFolder(
|
||||
query: FolderQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<ViewItem[] | null> {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
return hass ? this._executor.expandFolder(hass, query, engineOptions) : null;
|
||||
}
|
||||
|
||||
public getItemCapabilities(item: ViewItem): ViewItemCapabilities | null {
|
||||
return this._executor.getItemCapabilities(item);
|
||||
}
|
||||
|
||||
public async getDownloadPath(item: ViewItem): Promise<Endpoint | null> {
|
||||
return await this._executor.getDownloadPath(
|
||||
this._api.getHASSManager().getHASS(),
|
||||
item,
|
||||
{
|
||||
resolvedMediaCache: this._api.getResolvedMediaCache(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public async favorite(item: ViewItem, favorite: boolean): Promise<void> {
|
||||
return await this._executor.favorite(
|
||||
this._api.getHASSManager().getHASS(),
|
||||
item,
|
||||
favorite,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { NonEmptyTuple } from 'type-fest';
|
||||
import { FolderConfig, HAFolderPathComponent } from '../../config/schema/folders';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Endpoint } from '../../types';
|
||||
import { AdvancedCameraCardError } from '../../types.js';
|
||||
import { ViewItem } from '../../view/item';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
|
||||
// ====
|
||||
// Base
|
||||
// ====
|
||||
|
||||
export interface EngineOptions {
|
||||
useCache?: boolean;
|
||||
}
|
||||
|
||||
export class FolderInitializationError extends AdvancedCameraCardError {}
|
||||
|
||||
// ============
|
||||
// Folder Query
|
||||
// ============
|
||||
|
||||
export type FolderPathComponent = {
|
||||
id?: string;
|
||||
ha?: Omit<HAFolderPathComponent, 'id'>;
|
||||
};
|
||||
|
||||
export interface FolderQuery {
|
||||
folder: FolderConfig;
|
||||
|
||||
// A trail of paths to navigate back to the "root", with the last path being
|
||||
// the path that this query directly refers to.
|
||||
path: NonEmptyTuple<FolderPathComponent>;
|
||||
}
|
||||
|
||||
// ===============
|
||||
// Folders Engines
|
||||
// ===============
|
||||
|
||||
export interface DownloadHelpers {
|
||||
resolvedMediaCache?: ResolvedMediaCache | null;
|
||||
}
|
||||
|
||||
export interface FoldersEngine {
|
||||
generateDefaultFolderQuery(folder: FolderConfig): FolderQuery | null;
|
||||
expandFolder(
|
||||
hass: HomeAssistant,
|
||||
query: FolderQuery,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<ViewItem[] | null>;
|
||||
|
||||
getItemCapabilities(item: ViewItem): ViewItemCapabilities | null;
|
||||
getDownloadPath(
|
||||
hass: HomeAssistant | null,
|
||||
item: ViewItem,
|
||||
options?: DownloadHelpers,
|
||||
): Promise<Endpoint | null>;
|
||||
favorite(hass: HomeAssistant | null, item: ViewItem, favorite: boolean): Promise<void>;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { hasHAConnectionStateChanged } from '../../ha/has-hass-connection-changed';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { hasHAConnectionStateChanged } from '../../utils/ha';
|
||||
import { CardHASSAPI } from '../types';
|
||||
import { StateWatcher, StateWatcherSubscriptionInterface } from './state-watcher';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { getHassDifferences, HassStateDifference } from '../../utils/ha';
|
||||
import { getHassDifferences } from '../../ha/get-hass-differences';
|
||||
import { HassStateDifference, HomeAssistant } from '../../ha/types';
|
||||
|
||||
type StateWatcherCallback = (difference: HassStateDifference) => void;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import PQueue from 'p-queue';
|
||||
import { loadLanguages } from '../localize/localize';
|
||||
import { sideLoadHomeAssistantElements } from '../utils/ha';
|
||||
import { sideLoadHomeAssistantElements } from '../ha/side-load-ha-elements';
|
||||
import { Initializer } from '../utils/initializer/initializer';
|
||||
import { CardInitializerAPI } from './types';
|
||||
|
||||
@@ -42,6 +42,10 @@ export class InitializationManager {
|
||||
return this._everInitialized;
|
||||
}
|
||||
|
||||
public isInitialized(aspect: InitializationAspect): boolean {
|
||||
return this._initializer.isInitialized(aspect);
|
||||
}
|
||||
|
||||
public isInitializedMandatory(): boolean {
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
if (!config) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { setOrRemoveAttribute } from '../utils/basic';
|
||||
import { Timer } from '../utils/timer';
|
||||
import { CardInteractionAPI } from './types';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CardKeyboardStateAPI, KeysState } from './types';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
export class KeyboardStateManager {
|
||||
protected _api: CardKeyboardStateAPI;
|
||||
|
||||
@@ -5,12 +5,12 @@ import {
|
||||
MEDIA_PLAYER_SUPPORT_STOP,
|
||||
MEDIA_PLAYER_SUPPORT_TURN_OFF,
|
||||
} from '../const';
|
||||
import { Entity } from '../ha/registry/entity/types';
|
||||
import { supportsFeature } from '../ha/supports-feature';
|
||||
import { localize } from '../localize/localize';
|
||||
import { errorToConsole } from '../utils/basic';
|
||||
import { supportsFeature } from '../utils/ha';
|
||||
import { Entity } from '../utils/ha/registry/entity/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||
import { ViewMedia } from '../view/item';
|
||||
import { ViewItemClassifier } from '../view/item-classifier';
|
||||
import { CardMediaPlayerAPI } from './types';
|
||||
|
||||
export class MediaPlayerManager {
|
||||
@@ -196,7 +196,7 @@ export class MediaPlayerManager {
|
||||
await hass.callService('media_player', 'play_media', {
|
||||
entity_id: mediaPlayer,
|
||||
media_content_id: media.getContentID(),
|
||||
media_content_type: ViewMediaClassifier.isVideo(media) ? 'video' : 'image',
|
||||
media_content_type: ViewItemClassifier.isVideo(media) ? 'video' : 'image',
|
||||
extra: {
|
||||
...(title && { title: title }),
|
||||
...(thumbnail && { thumb: thumbnail }),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { StatusBarItem } from '../config/schema/actions/types';
|
||||
import { StatusBarConfig } from '../config/schema/status-bar';
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import throttle from 'lodash-es/throttle';
|
||||
import { orderBy, throttle } from 'lodash-es';
|
||||
import { CameraEvent } from '../camera-manager/types';
|
||||
import { Timer } from '../utils/timer';
|
||||
import { CardTriggersAPI } from './types';
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { CameraManager } from '../camera-manager/manager';
|
||||
import type { ConditionStateManager } from '../conditions/state-manager';
|
||||
import type { Automation } from '../config/schema/automations';
|
||||
import type { EntityRegistryManager } from '../utils/ha/registry/entity/types';
|
||||
import type { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import type { EntityRegistryManager } from '../ha/registry/entity/types';
|
||||
import type { ResolvedMediaCache } from '../ha/resolved-media';
|
||||
import type { ActionsManager } from './actions/actions-manager';
|
||||
import type { AutomationsManager } from './automations-manager';
|
||||
import type { CameraURLManager } from './camera-url-manager';
|
||||
import type { CardElementManager } from './card-element-manager';
|
||||
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 { FoldersManager } from './folders/manager';
|
||||
import type { FullscreenManager } from './fullscreen/fullscreen-manager';
|
||||
import type { HASSManager } from './hass/hass-manager';
|
||||
import type { InitializationManager } from './initialization-manager';
|
||||
@@ -24,6 +24,7 @@ import type { QueryStringManager } from './query-string-manager';
|
||||
import type { StatusBarItemManager } from './status-bar-item-manager';
|
||||
import type { StyleManager } from './style-manager';
|
||||
import type { TriggersManager } from './triggers-manager';
|
||||
import type { ViewItemManager } from './view/item-manager';
|
||||
import type { ViewManager } from './view/view-manager';
|
||||
|
||||
// *************************************************************************
|
||||
@@ -40,8 +41,8 @@ export interface CardActionsAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getDownloadManager(): DownloadManager;
|
||||
getExpandManager(): ExpandManager;
|
||||
getFoldersManager(): FoldersManager;
|
||||
getFullscreenManager(): FullscreenManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
@@ -50,6 +51,7 @@ export interface CardActionsAPI {
|
||||
getMicrophoneManager(): MicrophoneManager;
|
||||
getStatusBarItemManager(): StatusBarItemManager;
|
||||
getTriggersManager(): TriggersManager;
|
||||
getViewItemManager(): ViewItemManager;
|
||||
getViewManager(): ViewManager;
|
||||
}
|
||||
export type CardActionsManagerAPI = CardActionsAPI;
|
||||
@@ -90,6 +92,7 @@ export interface CardConfigAPI {
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getDefaultManager(): DefaultManager;
|
||||
getFoldersManager(): FoldersManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getInitializationManager(): InitializationManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
@@ -104,6 +107,8 @@ export interface CardConfigAPI {
|
||||
export interface CardConfigLoaderAPI {
|
||||
getAutomationsManager(): AutomationsManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getFoldersManager(): FoldersManager;
|
||||
getMessageManager(): MessageManager;
|
||||
getHASSManager(): HASSManager;
|
||||
}
|
||||
|
||||
@@ -148,6 +153,12 @@ export interface CardExpandAPI {
|
||||
getFullscreenManager(): FullscreenManager;
|
||||
}
|
||||
|
||||
export interface CardFoldersAPI {
|
||||
getConfigManager(): ConfigManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getResolvedMediaCache(): ResolvedMediaCache;
|
||||
}
|
||||
|
||||
export interface CardFullscreenAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
@@ -270,7 +281,9 @@ export interface CardViewAPI {
|
||||
getCardElementManager(): CardElementManager;
|
||||
getConditionStateManager(): ConditionStateManager;
|
||||
getConfigManager(): ConfigManager;
|
||||
getFoldersManager(): FoldersManager;
|
||||
getHASSManager(): HASSManager;
|
||||
getInitializationManager(): InitializationManager;
|
||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||
getMessageManager(): MessageManager;
|
||||
getQueryStringManager(): QueryStringManager;
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { format } from 'date-fns';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { AdvancedCameraCardError } from '../../types';
|
||||
import { errorToConsole } from '../../utils/basic';
|
||||
import { downloadURL } from '../../utils/download';
|
||||
import { homeAssistantSignPath } from '../../ha/sign-path';
|
||||
import { ViewItem } from '../../view/item';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
import { CardViewAPI } from '../types';
|
||||
|
||||
enum ViewMediaSource {
|
||||
Camera = 'camera',
|
||||
Folder = 'folder',
|
||||
}
|
||||
|
||||
export class ViewItemManager {
|
||||
private _api: CardViewAPI;
|
||||
|
||||
constructor(api: CardViewAPI) {
|
||||
this._api = api;
|
||||
}
|
||||
|
||||
public getCapabilities(item: ViewItem): ViewItemCapabilities | null {
|
||||
const source = this._getMediaSource(item);
|
||||
if (source === ViewMediaSource.Camera && ViewItemClassifier.isMedia(item)) {
|
||||
return this._api.getCameraManager().getMediaCapabilities(item);
|
||||
}
|
||||
if (source === ViewMediaSource.Folder) {
|
||||
return this._api.getFoldersManager().getItemCapabilities(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public async download(item: ViewItem): Promise<boolean> {
|
||||
try {
|
||||
await this._download(item);
|
||||
} catch (error: unknown) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(error);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async favorite(item: ViewItem, favorite: boolean): Promise<void> {
|
||||
const source = this._getMediaSource(item);
|
||||
if (source === ViewMediaSource.Camera && ViewItemClassifier.isMedia(item)) {
|
||||
return await this._api.getCameraManager().favoriteMedia(item, favorite);
|
||||
}
|
||||
/* istanbul ignore else: this path cannot be reached -- @preserve */
|
||||
if (source === ViewMediaSource.Folder) {
|
||||
return this._api.getFoldersManager().favorite(item, favorite);
|
||||
}
|
||||
}
|
||||
|
||||
private _getMediaSource(item: ViewItem): ViewMediaSource | null {
|
||||
if (ViewItemClassifier.isMedia(item) && item.getCameraID()) {
|
||||
return ViewMediaSource.Camera;
|
||||
}
|
||||
if (ViewItemClassifier.isFolder(item) || item.getFolder()) {
|
||||
return ViewMediaSource.Folder;
|
||||
}
|
||||
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
return null;
|
||||
}
|
||||
|
||||
private async _download(item: ViewItem): Promise<void> {
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
if (!hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = this._getMediaSource(item);
|
||||
const endpoint =
|
||||
source === ViewMediaSource.Camera && ViewItemClassifier.isMedia(item)
|
||||
? await this._api.getCameraManager().getMediaDownloadPath(item)
|
||||
: source === ViewMediaSource.Folder
|
||||
? await this._api.getFoldersManager().getDownloadPath(item)
|
||||
: null;
|
||||
|
||||
if (!endpoint) {
|
||||
throw new AdvancedCameraCardError(localize('error.download_no_media'));
|
||||
}
|
||||
|
||||
let finalURL = endpoint.endpoint;
|
||||
if (endpoint.sign) {
|
||||
let response: string | null | undefined;
|
||||
try {
|
||||
response = await homeAssistantSignPath(hass, endpoint.endpoint);
|
||||
} catch (e) {
|
||||
errorToConsole(e as Error);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
throw new AdvancedCameraCardError(localize('error.download_sign_failed'));
|
||||
}
|
||||
finalURL = response;
|
||||
}
|
||||
|
||||
downloadURL(finalURL, this._generateDownloadFilename(item));
|
||||
}
|
||||
|
||||
private _generateDownloadFilename(item: ViewItem): string {
|
||||
const toFilename = (input: string): string => {
|
||||
return input.toLowerCase().replaceAll(/[^a-z0-9]/gi, '-');
|
||||
};
|
||||
|
||||
if (ViewItemClassifier.isMedia(item)) {
|
||||
const cameraID = item.getCameraID();
|
||||
const id = item.getID();
|
||||
const startTime = item.getStartTime();
|
||||
|
||||
return (
|
||||
(cameraID ? toFilename(cameraID) : 'media') +
|
||||
(id ? `_${toFilename(id)}` : '') +
|
||||
(startTime ? `_${format(startTime, `yyyy-MM-dd-HH-mm-ss`)}` : '') +
|
||||
('.' + (item.getMediaType() === 'clip' ? 'mp4' : 'jpg'))
|
||||
);
|
||||
}
|
||||
|
||||
/* istanbul ignore else: this path cannot be reached -- @preserve */
|
||||
if (ViewItemClassifier.isFolder(item)) {
|
||||
return toFilename(item.getTitle() ?? 'media');
|
||||
}
|
||||
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
return 'download';
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
import { MediaQueries } from '../../../view/media-queries';
|
||||
import { MediaQueriesResults } from '../../../view/media-queries-results';
|
||||
import { Query } from '../../../view/query';
|
||||
import { QueryResults } from '../../../view/query-results';
|
||||
import { View } from '../../../view/view';
|
||||
import { ViewModifier } from '../types';
|
||||
|
||||
export class SetQueryViewModifier implements ViewModifier {
|
||||
protected _query?: MediaQueries | null;
|
||||
protected _queryResults?: MediaQueriesResults | null;
|
||||
protected _query?: Query | null;
|
||||
protected _queryResults?: QueryResults | null;
|
||||
|
||||
constructor(options?: {
|
||||
query?: MediaQueries | null;
|
||||
queryResults?: MediaQueriesResults | null;
|
||||
}) {
|
||||
constructor(options?: { query?: Query | null; queryResults?: QueryResults | null }) {
|
||||
this._query = options?.query;
|
||||
this._queryResults = options?.queryResults;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { CapabilitySearchOptions, MediaQuery } from '../../camera-manager/types';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../../const';
|
||||
import { ClipsOrSnapshotsOrAll } from '../../types';
|
||||
import { findBestMediaIndex } from '../../utils/find-best-media-index';
|
||||
import { findBestMediaTimeIndex } from '../../utils/find-best-media-time-index';
|
||||
import { ViewItem } from '../../view/item';
|
||||
import {
|
||||
EventMediaQueries,
|
||||
EventMediaQuery,
|
||||
FolderViewQuery,
|
||||
MediaQueries,
|
||||
RecordingMediaQueries,
|
||||
} from '../../view/media-queries';
|
||||
import { MediaQueriesResults } from '../../view/media-queries-results';
|
||||
Query,
|
||||
RecordingMediaQuery,
|
||||
} from '../../view/query';
|
||||
import { QueryClassifier } from '../../view/query-classifier';
|
||||
import { QueryResults } from '../../view/query-results';
|
||||
import { CardViewAPI } from '../types';
|
||||
import { QueryExecutorOptions, QueryExecutorResult } from './types';
|
||||
|
||||
@@ -48,14 +52,8 @@ export class QueryExecutor {
|
||||
if (!rawQueries) {
|
||||
return null;
|
||||
}
|
||||
const queries = new EventMediaQueries(rawQueries);
|
||||
const results = await this.execute(queries, options?.executorOptions);
|
||||
return results
|
||||
? {
|
||||
query: queries,
|
||||
queryResults: results,
|
||||
}
|
||||
: null;
|
||||
const queries = new EventMediaQuery(rawQueries);
|
||||
return await this.executeMediaQuery(queries, options?.executorOptions);
|
||||
}
|
||||
|
||||
public async executeDefaultRecordingQuery(options?: {
|
||||
@@ -76,16 +74,30 @@ export class QueryExecutor {
|
||||
if (!rawQueries) {
|
||||
return null;
|
||||
}
|
||||
const queries = new RecordingMediaQueries(rawQueries);
|
||||
const results = await this.execute(queries, options?.executorOptions);
|
||||
return results ? { query: queries, queryResults: results } : null;
|
||||
const queries = new RecordingMediaQuery(rawQueries);
|
||||
return await this.executeMediaQuery(queries, options?.executorOptions);
|
||||
}
|
||||
|
||||
public async execute(
|
||||
public async executeQuery(
|
||||
query: Query,
|
||||
executorOptions?: QueryExecutorOptions,
|
||||
): Promise<QueryExecutorResult | null> {
|
||||
/* istanbul ignore else: this path cannot be reached -- @preserve */
|
||||
if (QueryClassifier.isMediaQuery(query)) {
|
||||
return await this.executeMediaQuery(query, executorOptions);
|
||||
} else if (QueryClassifier.isFolderQuery(query)) {
|
||||
return await this._executeFolderQuery(query, executorOptions);
|
||||
}
|
||||
|
||||
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||
return null;
|
||||
}
|
||||
|
||||
public async executeMediaQuery(
|
||||
query: MediaQueries,
|
||||
executorOptions?: QueryExecutorOptions,
|
||||
): Promise<MediaQueriesResults | null> {
|
||||
const queries = query.getQueries();
|
||||
): Promise<QueryExecutorResult | null> {
|
||||
const queries = query.getQuery();
|
||||
if (!queries) {
|
||||
return null;
|
||||
}
|
||||
@@ -95,11 +107,17 @@ export class QueryExecutor {
|
||||
.executeMediaQueries<MediaQuery>(queries, {
|
||||
useCache: executorOptions?.useCache,
|
||||
});
|
||||
if (!mediaArray) {
|
||||
return null;
|
||||
}
|
||||
const queryResults = mediaArray
|
||||
? this._generateQueriesResults(mediaArray, executorOptions)
|
||||
: null;
|
||||
return queryResults ? { query, queryResults } : null;
|
||||
}
|
||||
|
||||
const queryResults = new MediaQueriesResults({ results: mediaArray });
|
||||
private _generateQueriesResults(
|
||||
itemArray: ViewItem[],
|
||||
executorOptions?: QueryExecutorOptions,
|
||||
): QueryResults | null {
|
||||
const queryResults = new QueryResults({ results: itemArray });
|
||||
if (executorOptions?.rejectResults?.(queryResults)) {
|
||||
return null;
|
||||
}
|
||||
@@ -111,9 +129,9 @@ export class QueryExecutor {
|
||||
} else if (executorOptions?.selectResult?.func) {
|
||||
queryResults.selectResultIfFound(executorOptions.selectResult.func);
|
||||
} else if (executorOptions?.selectResult?.time) {
|
||||
queryResults.selectBestResult((media) =>
|
||||
findBestMediaIndex(
|
||||
media,
|
||||
queryResults.selectBestResult((itemArray) =>
|
||||
findBestMediaTimeIndex(
|
||||
itemArray,
|
||||
executorOptions.selectResult?.time?.time as Date,
|
||||
executorOptions.selectResult?.time?.favorCameraID,
|
||||
),
|
||||
@@ -122,6 +140,33 @@ export class QueryExecutor {
|
||||
return queryResults;
|
||||
}
|
||||
|
||||
public async executeDefaultFolderQuery(
|
||||
executorOptions?: QueryExecutorOptions,
|
||||
): Promise<QueryExecutorResult | null> {
|
||||
const query = this._api.getFoldersManager().generateDefaultFolderQuery();
|
||||
return query
|
||||
? this._executeFolderQuery(new FolderViewQuery(query), executorOptions)
|
||||
: null;
|
||||
}
|
||||
|
||||
private async _executeFolderQuery(
|
||||
query: FolderViewQuery,
|
||||
executorOptions?: QueryExecutorOptions,
|
||||
): Promise<QueryExecutorResult | null> {
|
||||
const rawQuery = query.getQuery();
|
||||
if (!rawQuery) {
|
||||
return null;
|
||||
}
|
||||
const itemArray = await this._api
|
||||
.getFoldersManager()
|
||||
.expandFolder(rawQuery, { useCache: executorOptions?.useCache });
|
||||
|
||||
const queryResults = itemArray
|
||||
? this._generateQueriesResults(itemArray, executorOptions)
|
||||
: null;
|
||||
return queryResults ? { query, queryResults } : null;
|
||||
}
|
||||
|
||||
protected _getChunkLimit(): number {
|
||||
const cardWideConfig = this._api.getConfigManager().getCardWideConfig();
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { orderBy, uniqBy } from 'lodash-es';
|
||||
import { ViewItem } from '../../view/item';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier';
|
||||
|
||||
export const sortItems = <T extends ViewItem>(itemArray: T[]): T[] => {
|
||||
return orderBy(
|
||||
// Ensure uniqueness by the ID (if specified), otherwise all elements
|
||||
// are assumed to be unique.
|
||||
uniqBy(itemArray, (item) => item.getID() ?? item),
|
||||
|
||||
[
|
||||
// Pull folders to the front.
|
||||
(item) => !ViewItemClassifier.isFolder(item),
|
||||
|
||||
// Sort by time and id.
|
||||
(item) =>
|
||||
ViewItemClassifier.isMedia(item)
|
||||
? item.getStartTime() ?? item.getID()
|
||||
: item.getID(),
|
||||
],
|
||||
['asc', 'asc'],
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ViewContext } from 'view';
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const.js';
|
||||
import { AdvancedCameraCardError } from '../../types.js';
|
||||
import { MediaQueriesResults } from '../../view/media-queries-results.js';
|
||||
import { MediaQueries } from '../../view/media-queries.js';
|
||||
import { ViewMedia } from '../../view/media.js';
|
||||
import { ViewItem } from '../../view/item.js';
|
||||
import { QueryResults } from '../../view/query-results.js';
|
||||
import { Query } from '../../view/query.js';
|
||||
import { View, ViewParameters } from '../../view/view.js';
|
||||
|
||||
export interface ViewModifier {
|
||||
@@ -20,15 +20,15 @@ export interface QueryExecutorOptions {
|
||||
favorCameraID?: string;
|
||||
};
|
||||
id?: string;
|
||||
func?: (media: ViewMedia) => boolean;
|
||||
func?: (media: ViewItem) => boolean;
|
||||
};
|
||||
rejectResults?: (results: MediaQueriesResults) => boolean;
|
||||
rejectResults?: (results: QueryResults) => boolean;
|
||||
useCache?: boolean;
|
||||
}
|
||||
|
||||
export interface QueryExecutorResult {
|
||||
query: MediaQueries;
|
||||
queryResults: MediaQueriesResults;
|
||||
query: Query;
|
||||
queryResults: QueryResults;
|
||||
}
|
||||
|
||||
export interface ViewFactoryOptions {
|
||||
|
||||
@@ -2,9 +2,10 @@ import { ViewContext } from 'view';
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const';
|
||||
import { log } from '../../utils/debug';
|
||||
import { getStreamCameraID } from '../../utils/substream';
|
||||
import { MediaQueriesClassifier } from '../../view/media-queries-classifier';
|
||||
import { QueryClassifier } from '../../view/query-classifier';
|
||||
import { View } from '../../view/view';
|
||||
import { getCameraIDsForViewName } from '../../view/view-to-cameras';
|
||||
import { InitializationAspect } from '../initialization-manager';
|
||||
import { CardViewAPI } from '../types';
|
||||
import { ViewFactory } from './factory';
|
||||
import { applyViewModifiers } from './modifiers';
|
||||
@@ -103,6 +104,10 @@ export class ViewManager implements ViewManagerInterface {
|
||||
viewFactoryFunc: (options?: ViewFactoryOptions) => View | null,
|
||||
options?: ViewFactoryOptions,
|
||||
): void {
|
||||
if (!this._isAllowedToSetView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let view: View | null = null;
|
||||
try {
|
||||
view = viewFactoryFunc({
|
||||
@@ -112,7 +117,9 @@ export class ViewManager implements ViewManagerInterface {
|
||||
} catch (e) {
|
||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||
}
|
||||
view && this._setView(view);
|
||||
if (view) {
|
||||
this._setView(view);
|
||||
}
|
||||
}
|
||||
|
||||
protected _markViewLoadingQuery(view: View, index: number): View {
|
||||
@@ -122,6 +129,19 @@ export class ViewManager implements ViewManagerInterface {
|
||||
return view.removeContextProperty('loading', 'query');
|
||||
}
|
||||
|
||||
protected _isAllowedToSetView(): boolean {
|
||||
// It is possible to have a race condition where the view is being set at
|
||||
// the same time as the cameras being initialized. Test case: Open
|
||||
// folder-based media in the media viewer carousel, then attempt to edit the
|
||||
// card -- this causes the cameras to re-initialize at the same time as
|
||||
// folder media is reporting observed zoom settings in the view context.
|
||||
// Without this check, that will result in a "No cameras support this view"
|
||||
// message.
|
||||
return this._api
|
||||
.getInitializationManager()
|
||||
.isInitialized(InitializationAspect.CAMERAS);
|
||||
}
|
||||
|
||||
protected async _setViewThenModifyAsync(
|
||||
viewFactoryFunc: (options?: ViewFactoryOptions) => View | null,
|
||||
viewModifiersFunc: (
|
||||
@@ -130,6 +150,10 @@ export class ViewManager implements ViewManagerInterface {
|
||||
) => Promise<ViewModifier[] | null>,
|
||||
options?: ViewFactoryOptions,
|
||||
): Promise<void> {
|
||||
if (!this._isAllowedToSetView()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let initialView: View | null = null;
|
||||
try {
|
||||
initialView = viewFactoryFunc({
|
||||
@@ -220,10 +244,10 @@ export class ViewManager implements ViewManagerInterface {
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/885
|
||||
|
||||
const switchingFromViewerToGallery =
|
||||
this._view?.isViewerView() && newView?.isGalleryView();
|
||||
this._view?.isViewerView() && newView?.isMediaGalleryView();
|
||||
const newMediaType = newView?.getDefaultMediaType();
|
||||
const alreadyHasMatchingQuery =
|
||||
MediaQueriesClassifier.getMediaType(this._view?.query) === newMediaType;
|
||||
QueryClassifier.getMediaType(this._view?.query) === newMediaType;
|
||||
return !!switchingFromViewerToGallery && alreadyHasMatchingQuery;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,9 @@ export class ViewQueryExecutor {
|
||||
return view.query
|
||||
? [
|
||||
new SetQueryViewModifier({
|
||||
queryResults: await this._executor.execute(view.query, queryExecutorOptions),
|
||||
queryResults: (
|
||||
await this._executor.executeQuery(view.query, queryExecutorOptions)
|
||||
)?.queryResults,
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
@@ -83,6 +85,12 @@ export class ViewQueryExecutor {
|
||||
return results ? [new SetQueryViewModifier(results)] : [];
|
||||
};
|
||||
|
||||
const executeFolderQuery = async (): Promise<ViewModifier[]> => {
|
||||
const results =
|
||||
await this._executor.executeDefaultFolderQuery(queryExecutorOptions);
|
||||
return results ? [new SetQueryViewModifier(results)] : [];
|
||||
};
|
||||
|
||||
switch (view.view) {
|
||||
case 'live':
|
||||
if (config.live.controls.thumbnails.mode !== 'none') {
|
||||
@@ -111,6 +119,9 @@ export class ViewQueryExecutor {
|
||||
case 'recordings':
|
||||
viewModifiers.push(...(await executeMediaQuery(mediaType)));
|
||||
break;
|
||||
case 'folder':
|
||||
viewModifiers.push(...(await executeFolderQuery()));
|
||||
break;
|
||||
}
|
||||
|
||||
viewModifiers.push(...this._getTimelineWindowViewModifier(view));
|
||||
|
||||
Reference in New Issue
Block a user