feat: Implement basic general folder support (#2051)

- Related: #1748
This commit is contained in:
Dermot Duffy
2025-05-21 19:59:21 -07:00
committed by GitHub
parent 2eb0d9e35e
commit c6a4c8aea2
350 changed files with 12837 additions and 4509 deletions
@@ -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,
+12 -6
View File
@@ -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 -1
View File
@@ -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);
+3
View File
@@ -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';
+3 -2
View File
@@ -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 -1
View File
@@ -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';
+3 -1
View File
@@ -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);
}
};
+19 -22
View File
@@ -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 -1
View File
@@ -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';
-41
View File
@@ -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()));
}
}
}
+74
View File
@@ -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;
}
}
+180
View File
@@ -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,
},
};
}
}
+90
View File
@@ -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,
);
}
}
+60
View File
@@ -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 -1
View File
@@ -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';
+2 -2
View File
@@ -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 -1
View File
@@ -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 -5
View File
@@ -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 -2
View File
@@ -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';
+17 -4
View File
@@ -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;
+131
View File
@@ -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;
}
+71 -26
View File
@@ -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 (
+23
View File
@@ -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'],
);
};
+7 -7
View File
@@ -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 {
+28 -4
View File
@@ -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));