fix: Timeline should should show folder media even without camera support (#2216)
- Closes #2205
This commit is contained in:
@@ -3,7 +3,11 @@ import { ViewDisplayMode } from '../../config/schema/common/display';
|
||||
import { AdvancedCameraCardConfig } from '../../config/schema/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { View, ViewParameters } from '../../view/view';
|
||||
import { getCameraIDsForViewName } from '../../view/view-to-cameras';
|
||||
import {
|
||||
getCameraIDsForViewName,
|
||||
isViewSupportedByCamera,
|
||||
isViewSupportedByQueryOnly,
|
||||
} from '../../view/view-support';
|
||||
import { CardViewAPI } from '../types';
|
||||
import { applyViewModifiers } from './modifiers';
|
||||
import { ViewFactoryOptions, ViewIncompatible, ViewNoCameraError } from './types';
|
||||
@@ -32,7 +36,7 @@ export class ViewFactory {
|
||||
cameraID = options.params.camera;
|
||||
} else {
|
||||
const cameraIDs = [
|
||||
...getCameraIDsForViewName(this._api.getCameraManager(), viewName),
|
||||
...getCameraIDsForViewName(viewName, this._api.getCameraManager()),
|
||||
];
|
||||
|
||||
if (
|
||||
@@ -73,8 +77,8 @@ export class ViewFactory {
|
||||
|
||||
if (!cameraID || !allCameraIDs.has(cameraID)) {
|
||||
const viewCameraIDs = getCameraIDsForViewName(
|
||||
this._api.getCameraManager(),
|
||||
viewName,
|
||||
this._api.getCameraManager(),
|
||||
);
|
||||
|
||||
// Reset to the default camera.
|
||||
@@ -98,8 +102,18 @@ export class ViewFactory {
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isViewSupportedByCamera(cameraID, viewName)) {
|
||||
if (options?.failSafe && this.isViewSupportedByCamera(cameraID, VIEW_DEFAULT)) {
|
||||
if (
|
||||
!isViewSupportedByCamera(viewName, this._api.getCameraManager(), cameraID) &&
|
||||
!isViewSupportedByQueryOnly(
|
||||
viewName,
|
||||
options?.params?.query ?? options?.baseView?.query,
|
||||
options?.params?.queryResults ?? options?.baseView?.queryResults,
|
||||
)
|
||||
) {
|
||||
if (
|
||||
options?.failSafe &&
|
||||
isViewSupportedByCamera(VIEW_DEFAULT, this._api.getCameraManager(), cameraID)
|
||||
) {
|
||||
viewName = VIEW_DEFAULT;
|
||||
} else {
|
||||
const capabilities = this._api
|
||||
@@ -142,13 +156,6 @@ export class ViewFactory {
|
||||
return view;
|
||||
}
|
||||
|
||||
public isViewSupportedByCamera(
|
||||
cameraID: string,
|
||||
view: AdvancedCameraCardView,
|
||||
): boolean {
|
||||
return !!getCameraIDsForViewName(this._api.getCameraManager(), view, cameraID).size;
|
||||
}
|
||||
|
||||
protected _getDefaultDisplayModeForView(
|
||||
viewName: AdvancedCameraCardView,
|
||||
config: AdvancedCameraCardConfig,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ViewContext } from 'view';
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const.js';
|
||||
import { AdvancedCameraCardError } from '../../types.js';
|
||||
import { ViewItem } from '../../view/item.js';
|
||||
import { QueryResults } from '../../view/query-results.js';
|
||||
@@ -73,7 +72,6 @@ export interface ViewManagerInterface {
|
||||
|
||||
setViewWithMergedContext(context: ViewContext | null): void;
|
||||
|
||||
isViewSupportedByCamera(cameraID: string, view: AdvancedCameraCardView): boolean;
|
||||
hasMajorMediaChange(oldView?: View | null, newView?: View | null): boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { ViewContext } from 'view';
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const';
|
||||
import { log } from '../../utils/debug';
|
||||
import { getStreamCameraID } from '../../utils/substream';
|
||||
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';
|
||||
@@ -257,13 +255,6 @@ export class ViewManager implements ViewManagerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
public isViewSupportedByCamera(
|
||||
cameraID: string,
|
||||
view: AdvancedCameraCardView,
|
||||
): boolean {
|
||||
return !!getCameraIDsForViewName(this._api.getCameraManager(), view, cameraID).size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if the current view has a major "media change" for the given previous view.
|
||||
* @param oldView The previous view.
|
||||
|
||||
@@ -29,7 +29,11 @@ import { getStreamCameraID, hasSubstream } from '../utils/substream';
|
||||
import { ViewItemClassifier } from '../view/item-classifier';
|
||||
import { QueryClassifier } from '../view/query-classifier';
|
||||
import { View } from '../view/view';
|
||||
import { getCameraIDsForViewName } from '../view/view-to-cameras';
|
||||
import {
|
||||
getCameraIDsForViewName,
|
||||
isViewSupportedByCamera,
|
||||
isViewSupportedByQueryOnly,
|
||||
} from '../view/view-support';
|
||||
|
||||
export interface MenuButtonControllerOptions {
|
||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||
@@ -73,12 +77,12 @@ export class MenuButtonController {
|
||||
this._getIrisButton(config),
|
||||
this._getCamerasButton(config, cameraManager, options?.view),
|
||||
this._getSubstreamsButton(config, cameraManager, options?.view),
|
||||
this._getLiveButton(config, options?.view, options?.viewManager),
|
||||
this._getClipsButton(config, options?.view, options?.viewManager),
|
||||
this._getSnapshotsButton(config, options?.view, options?.viewManager),
|
||||
this._getRecordingsButton(config, options?.view, options?.viewManager),
|
||||
this._getImageButton(config, options?.view, options?.viewManager),
|
||||
this._getTimelineButton(config, options?.view, options?.viewManager),
|
||||
this._getLiveButton(config, cameraManager, options?.view),
|
||||
this._getClipsButton(config, cameraManager, options?.view),
|
||||
this._getSnapshotsButton(config, cameraManager, options?.view),
|
||||
this._getRecordingsButton(config, cameraManager, options?.view),
|
||||
this._getImageButton(config, cameraManager, options?.view),
|
||||
this._getTimelineButton(config, cameraManager, options?.view),
|
||||
this._getDownloadButton(config, cameraManager, options?.view),
|
||||
this._getCameraUIButton(config, options?.showCameraUIButton),
|
||||
this._getMicrophoneButton(
|
||||
@@ -227,10 +231,10 @@ export class MenuButtonController {
|
||||
|
||||
protected _getLiveButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
viewManager?: ViewManager | null,
|
||||
): MenuItem | null {
|
||||
return view && viewManager?.isViewSupportedByCamera(view.camera, 'live')
|
||||
return view && isViewSupportedByCamera('live', cameraManager, view.camera)
|
||||
? {
|
||||
icon: 'mdi:cctv',
|
||||
...config.menu.buttons.live,
|
||||
@@ -244,10 +248,10 @@ export class MenuButtonController {
|
||||
|
||||
protected _getClipsButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
viewManager?: ViewManager | null,
|
||||
): MenuItem | null {
|
||||
return view && viewManager?.isViewSupportedByCamera(view.camera, 'clips')
|
||||
return view && isViewSupportedByCamera('clips', cameraManager, view.camera)
|
||||
? {
|
||||
icon: 'mdi:filmstrip',
|
||||
...config.menu.buttons.clips,
|
||||
@@ -262,10 +266,10 @@ export class MenuButtonController {
|
||||
|
||||
protected _getSnapshotsButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
viewManager?: ViewManager | null,
|
||||
): MenuItem | null {
|
||||
return view && viewManager?.isViewSupportedByCamera(view.camera, 'snapshots')
|
||||
return view && isViewSupportedByCamera('snapshots', cameraManager, view.camera)
|
||||
? {
|
||||
icon: 'mdi:camera',
|
||||
...config.menu.buttons.snapshots,
|
||||
@@ -280,10 +284,10 @@ export class MenuButtonController {
|
||||
|
||||
protected _getRecordingsButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
viewManager?: ViewManager | null,
|
||||
): MenuItem | null {
|
||||
return view && viewManager?.isViewSupportedByCamera(view.camera, 'recordings')
|
||||
return view && isViewSupportedByCamera('recordings', cameraManager, view.camera)
|
||||
? {
|
||||
icon: 'mdi:album',
|
||||
...config.menu.buttons.recordings,
|
||||
@@ -298,10 +302,10 @@ export class MenuButtonController {
|
||||
|
||||
protected _getImageButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
viewManager?: ViewManager | null,
|
||||
): MenuItem | null {
|
||||
return view && viewManager?.isViewSupportedByCamera(view.camera, 'image')
|
||||
return view && isViewSupportedByCamera('image', cameraManager, view.camera)
|
||||
? {
|
||||
icon: 'mdi:image',
|
||||
...config.menu.buttons.image,
|
||||
@@ -315,10 +319,14 @@ export class MenuButtonController {
|
||||
|
||||
protected _getTimelineButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
viewManager?: ViewManager | null,
|
||||
): MenuItem | null {
|
||||
return view && viewManager?.isViewSupportedByCamera(view.camera, 'timeline')
|
||||
const shouldShowTimelineButton =
|
||||
view &&
|
||||
(isViewSupportedByCamera('timeline', cameraManager, view.camera) ||
|
||||
isViewSupportedByQueryOnly('timeline', view.query, view.queryResults));
|
||||
return shouldShowTimelineButton
|
||||
? {
|
||||
icon: 'mdi:chart-gantt',
|
||||
...config.menu.buttons.timeline,
|
||||
@@ -545,7 +553,7 @@ export class MenuButtonController {
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
const viewCameraIDs = view
|
||||
? getCameraIDsForViewName(cameraManager, view.view)
|
||||
? getCameraIDsForViewName(view.view, cameraManager)
|
||||
: null;
|
||||
if (
|
||||
view?.supportsMultipleDisplayModes() &&
|
||||
|
||||
@@ -537,8 +537,13 @@ export class TimelineController {
|
||||
.resetSelectedResult()
|
||||
.selectResultIfFound((media) => media.getID() === properties.item, criteria);
|
||||
|
||||
const selectedItem = newResults?.getSelectedResult();
|
||||
const context: ViewContext = mergeViewContext(this._getTimelineContext(), {
|
||||
mediaViewer: { seek: properties.time },
|
||||
...(ViewItemClassifier.isEvent(selectedItem) &&
|
||||
// Only attempt to seek if the event has a real end time, otherwise
|
||||
// the viewer cannot actually seek there and shows the unseekable
|
||||
// message.
|
||||
selectedItem.getEndTime() && { mediaViewer: { seek: properties.time } }),
|
||||
});
|
||||
|
||||
if (!newResults || !newResults.hasSelectedResult()) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import { contentsChanged } from '../utils/basic';
|
||||
import './date-picker.js';
|
||||
import { AdvancedCameraCardDatePicker, DatePickerEvent } from './date-picker.js';
|
||||
import './icon';
|
||||
import { renderMessage } from './message';
|
||||
import './thumbnail/thumbnail.js';
|
||||
|
||||
/**
|
||||
@@ -139,7 +140,18 @@ export class AdvancedCameraCardTimelineCore extends LitElement {
|
||||
protected _controller: TimelineController = new TimelineController(this);
|
||||
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.timelineConfig || !this.keys?.length) {
|
||||
if (!this.hass || !this.timelineConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.keys.length) {
|
||||
if (!this.mini) {
|
||||
return renderMessage({
|
||||
message: localize('error.no_camera_or_media_for_timeline'),
|
||||
icon: 'mdi:chart-gantt',
|
||||
type: 'info',
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TimelineConfig } from '../config/schema/timeline';
|
||||
import { CardWideConfig } from '../config/schema/types';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import basicBlockStyle from '../scss/basic-block.scss';
|
||||
import { QueryClassifier } from '../view/query-classifier';
|
||||
import './surround.js';
|
||||
import './timeline-core.js';
|
||||
|
||||
@@ -32,6 +33,27 @@ export class AdvancedCameraCardTimeline extends LitElement {
|
||||
public cardWideConfig?: CardWideConfig;
|
||||
|
||||
protected _getKeys(): TimelineKey[] {
|
||||
const query = this.viewManagerEpoch?.manager.getView()?.query;
|
||||
|
||||
// If there's a query, try to extract camera IDs or folder info from it.
|
||||
if (QueryClassifier.isMediaQuery(query)) {
|
||||
const cameraIDs = query.getQueryCameraIDs();
|
||||
if (cameraIDs && cameraIDs.size) {
|
||||
return [...cameraIDs].map((cameraID) => ({ type: 'camera', cameraID }));
|
||||
}
|
||||
} else if (QueryClassifier.isFolderQuery(query)) {
|
||||
const folderConfig = query.getQuery()?.folder;
|
||||
if (folderConfig) {
|
||||
return [
|
||||
{
|
||||
type: 'folder' as const,
|
||||
folder: folderConfig,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise fall back to all cameras that support media queries.
|
||||
const keys: TimelineKey[] = [];
|
||||
for (const camera of this.cameraManager?.getStore().getCameraIDsWithCapability({
|
||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||
|
||||
@@ -699,6 +699,7 @@
|
||||
"no_camera_entity": "No s'ha pogut trobar l'entitat de la càmera",
|
||||
"no_camera_entity_for_triggers": "Es requereix una entitat de càmera per detectar automàticament els activadors",
|
||||
"no_camera_id": "No s'ha pogut determinar l'identificador de la càmera per a la següent càmera, és possible que hagis d'establir el paràmetre 'id' manualment",
|
||||
"no_camera_or_media_for_timeline": "",
|
||||
"no_dashboard_or_view": "Tant els paràmetres 'dashboard_path' com 'view_path' són necessaris per al mètode d'emissió 'dashboard'",
|
||||
"no_live_camera": "El paràmetre 'camera_entity' s'ha de configurar i validar per a aquest proveïdor en directe",
|
||||
"no_supported_camera": "",
|
||||
|
||||
@@ -699,9 +699,10 @@
|
||||
"no_camera_entity": "Could not find camera entity",
|
||||
"no_camera_entity_for_triggers": "A camera entity is required in order to autodetect triggers",
|
||||
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
|
||||
"no_camera_or_media_for_timeline": "No camera or media available for timeline",
|
||||
"no_dashboard_or_view": "Both 'dashboard_path' and 'view_path' parameters are required for the 'dashboard' cast method",
|
||||
"no_live_camera": "The camera_entity parameter must be set and valid for this live provider",
|
||||
"no_supported_camera": "The selected camera does not support this view",
|
||||
"no_supported_camera": "The selected camera or media does not support this view",
|
||||
"no_supported_cameras": "No cameras support this view",
|
||||
"reconnecting": "Reconnecting",
|
||||
"stream_not_loading": "The video stream has not yet loaded. This is could for any number of reasons. If configured (and by default), there will be an image refreshing every second until the stream loads correctly",
|
||||
|
||||
@@ -699,6 +699,7 @@
|
||||
"no_camera_entity": "Impossible de trouver l'entité de caméra",
|
||||
"no_camera_entity_for_triggers": "Une entité caméra est requise pour détecter automatiquement les déclencheurs",
|
||||
"no_camera_id": "Impossible de déterminer l'identifiant de la caméra suivante. Il faudra peut-être définir le paramètre « ID » manuellement",
|
||||
"no_camera_or_media_for_timeline": "",
|
||||
"no_dashboard_or_view": "Les paramètres 'dashboard_path' et 'view_path' sont requis pour la méthode de conversion 'dashboard'",
|
||||
"no_live_camera": "Le paramètre camera_entity doit être défini et valide pour ce fournisseur en direct",
|
||||
"no_supported_camera": "La caméra sélectionnée ne prend pas en charge cette vue",
|
||||
|
||||
@@ -699,6 +699,7 @@
|
||||
"no_camera_entity": "Impossibile trovare l'entità fotocamera",
|
||||
"no_camera_entity_for_triggers": "È necessaria un'entità telecamera per rilevare automaticamente i trigger",
|
||||
"no_camera_id": "Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'",
|
||||
"no_camera_or_media_for_timeline": "",
|
||||
"no_dashboard_or_view": "",
|
||||
"no_live_camera": "Il parametro fotocamera_enty deve essere impostato e valido per questo provider live",
|
||||
"no_supported_camera": "",
|
||||
|
||||
@@ -699,6 +699,7 @@
|
||||
"no_camera_entity": "Não foi possível encontrar a entidade da câmera",
|
||||
"no_camera_entity_for_triggers": "Uma entidade de câmera é necessária para detectar automaticamente os gatilhos",
|
||||
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
||||
"no_camera_or_media_for_timeline": "",
|
||||
"no_dashboard_or_view": "",
|
||||
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este provedor ativo",
|
||||
"no_supported_camera": "",
|
||||
|
||||
@@ -699,6 +699,7 @@
|
||||
"no_camera_entity": "Não existe uma entidade câmera",
|
||||
"no_camera_entity_for_triggers": "Não existe camera para a acção",
|
||||
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
||||
"no_camera_or_media_for_timeline": "",
|
||||
"no_dashboard_or_view": "",
|
||||
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este serviço ativo",
|
||||
"no_supported_camera": "",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { CapabilitySearchOptions } from '../camera-manager/types';
|
||||
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
||||
import { Query } from './query';
|
||||
import { QueryResults } from './query-results';
|
||||
|
||||
/**
|
||||
* Get cameraIDs that are relevant for a given view name based on camera
|
||||
* capability (if camera specified).
|
||||
*/
|
||||
export const getCameraIDsForViewName = (
|
||||
cameraManager: CameraManager,
|
||||
viewName: AdvancedCameraCardView,
|
||||
cameraManager: CameraManager,
|
||||
cameraID?: string,
|
||||
): Set<string> => {
|
||||
switch (viewName) {
|
||||
@@ -47,3 +49,27 @@ export const getCameraIDsForViewName = (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const isViewSupportedByCamera = (
|
||||
view: AdvancedCameraCardView,
|
||||
cameraManager: CameraManager,
|
||||
cameraID: string,
|
||||
): boolean => {
|
||||
return !!getCameraIDsForViewName(view, cameraManager, cameraID).size;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether a view is supported by a given query ONLY (i.e. regardless of what
|
||||
* the camera supports).
|
||||
*/
|
||||
export const isViewSupportedByQueryOnly = (
|
||||
view: AdvancedCameraCardView,
|
||||
query?: Query | null,
|
||||
queryResults?: QueryResults | null,
|
||||
): boolean => {
|
||||
switch (view) {
|
||||
case 'timeline':
|
||||
return !!query?.getQuery() && !!queryResults?.hasResults();
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Reference in New Issue
Block a user