Skip to content

Commit 421de8c

Browse files
committed
Refactors view creation to support sections
Updates view creation to leverage sections for improved layout control, especially on the home view. The home view now uses a grid layout via sections. This change makes it easier to create complex layouts and improves responsiveness. Previously, the card creation logic was embedded directly within each view, leading to code duplication and difficulty in customizing layouts. The changes introduce the concept of sections, which are distinct areas within a view that can contain multiple cards. The home view is updated to use sections for the persons, areas, and quick access cards. This makes it easier to create responsive layouts that adapt to different screen sizes.
1 parent 2525869 commit 421de8c

5 files changed

Lines changed: 137 additions & 99 deletions

File tree

dist/mushroom-strategy.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/mushroom-strategy.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
ViewInfo,
1515
} from './types/strategy/strategy-generics';
1616
import { sanitizeClassName } from './utilities/auxiliaries';
17-
import { logMessage, lvlError, lvlInfo } from './utilities/debug';
17+
import { logMessage, lvlError } from './utilities/debug';
1818
import RegistryFilter from './utilities/RegistryFilter';
1919
import { stackHorizontal } from './utilities/cardStacking';
2020
import { PersistentNotification } from './utilities/PersistentNotification';
@@ -57,13 +57,8 @@ class MushroomStrategy extends HTMLTemplateElement {
5757
const moduleName = sanitizeClassName(`${viewName}View`);
5858
const View = (await import(`./views/${moduleName}`)).default;
5959
const currentView = new View(Registry.strategyOptions.views[viewName]);
60-
const viewConfiguration = await currentView.getView();
6160

62-
if (viewConfiguration.cards.length) {
63-
return viewConfiguration;
64-
}
65-
66-
logMessage(lvlInfo, `View ${viewName} has no entities available!`);
61+
return await currentView.getView();
6762
} catch (e) {
6863
logMessage(lvlError, `Error importing ${viewName} view!`, e);
6964
}

src/utilities/cardStacking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function stackHorizontal(
2626
defaultCount: number = 2,
2727
columnCounts?: {
2828
[key: string]: number | undefined;
29-
},
29+
}
3030
): LovelaceCardConfig[] {
3131
if (cardConfigurations.length <= 1) {
3232
return cardConfigurations;

src/views/AbstractView.ts

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { sanitizeClassName } from '../utilities/auxiliaries';
1111
import { logMessage, lvlFatal } from '../utilities/debug';
1212
import RegistryFilter from '../utilities/RegistryFilter';
1313
import { stackHorizontal } from '../utilities/cardStacking';
14+
import { LovelaceSectionRawConfig } from '../types/homeassistant/data/lovelace/config/section';
1415

1516
/**
1617
* Abstract View Class.
@@ -34,10 +35,6 @@ abstract class AbstractView {
3435
type: '',
3536
};
3637

37-
protected get domain(): SupportedDomains | 'home' {
38-
return (this.constructor as unknown as ViewConstructor).domain;
39-
}
40-
4138
/**
4239
* Class constructor.
4340
*
@@ -50,10 +47,40 @@ abstract class AbstractView {
5047
}
5148
}
5249

50+
protected get domain(): SupportedDomains | 'home' {
51+
return (this.constructor as unknown as ViewConstructor).domain;
52+
}
53+
54+
// noinspection JSUnusedGlobalSymbols Methodd is dynamically called.
55+
/**
56+
* Get a view configuration.
57+
*
58+
* The configuration includes the card configurations which are created by createCardConfigurations().
59+
*/
60+
async getView(): Promise<LovelaceViewConfig | false> {
61+
const sectionsCards = await this.createSections();
62+
63+
if (!sectionsCards.length) {
64+
return false;
65+
}
66+
67+
if (this.domain === 'home') {
68+
return {
69+
...this.baseConfiguration,
70+
sections: sectionsCards,
71+
};
72+
}
73+
74+
return {
75+
...this.baseConfiguration,
76+
cards: sectionsCards as LovelaceCardConfig[],
77+
};
78+
}
79+
5380
/**
5481
* Create the configuration of the cards to include in the view.
5582
*/
56-
protected async createCardConfigurations(): Promise<LovelaceCardConfig[]> {
83+
protected async createSections(): Promise<LovelaceSectionRawConfig[]> {
5784
const viewCards: LovelaceCardConfig[] = [];
5885
const moduleName = sanitizeClassName(this.domain + 'Card');
5986
const DomainCard = (await import(`../cards/${moduleName}`)).default;
@@ -82,16 +109,16 @@ abstract class AbstractView {
82109
// Create a card configuration for each entity in the current area.
83110
areaCards.push(
84111
...areaEntities.map((entity) =>
85-
new DomainCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard(),
86-
),
112+
new DomainCard(entity, Registry.strategyOptions.card_options?.[entity.entity_id]).getCard()
113+
)
87114
);
88115

89116
// Stack the cards of the current area.
90117
if (areaCards.length) {
91118
areaCards = stackHorizontal(
92119
areaCards,
93120
Registry.strategyOptions.domains[this.domain as SupportedDomains].stack_count ??
94-
Registry.strategyOptions.domains['_'].stack_count,
121+
Registry.strategyOptions.domains['_'].stack_count
95122
);
96123

97124
// Create and insert a Header card.
@@ -113,29 +140,6 @@ abstract class AbstractView {
113140
return viewCards;
114141
}
115142

116-
/**
117-
* Get a view configuration.
118-
*
119-
* The configuration includes the card configurations which are created by createCardConfigurations().
120-
*/
121-
async getView(): Promise<LovelaceViewConfig> {
122-
return {
123-
...this.baseConfiguration,
124-
cards: await this.createCardConfigurations(),
125-
};
126-
}
127-
128-
/**
129-
* Get the domain's entity ids to target for a HASS service call.
130-
*/
131-
private getDomainTargets(): HassServiceTarget {
132-
return {
133-
entity_id: Registry.entities
134-
.filter((entity) => entity.entity_id.startsWith(this.domain + '.'))
135-
.map((entity) => entity.entity_id),
136-
};
137-
}
138-
139143
/**
140144
* Initialize the view configuration with defaults and custom settings.
141145
*
@@ -146,7 +150,7 @@ abstract class AbstractView {
146150
protected initializeViewConfig(
147151
viewConfiguration: ViewConfig,
148152
customConfiguration: ViewConfig = {},
149-
headerCardConfig: CustomHeaderCardConfig,
153+
headerCardConfig: CustomHeaderCardConfig
150154
): void {
151155
this.baseConfiguration = { ...this.baseConfiguration, ...viewConfiguration, ...customConfiguration };
152156

@@ -162,6 +166,17 @@ abstract class AbstractView {
162166
...headerCardConfig,
163167
}).createCard();
164168
}
169+
170+
/**
171+
* Get the domain's entity ids to target for a HASS service call.
172+
*/
173+
private getDomainTargets(): HassServiceTarget {
174+
return {
175+
entity_id: Registry.entities
176+
.filter((entity) => entity.entity_id.startsWith(this.domain + '.'))
177+
.map((entity) => entity.entity_id),
178+
};
179+
}
165180
}
166181

167182
export default AbstractView;

src/views/HomeView.ts

Lines changed: 87 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// noinspection JSUnusedGlobalSymbols Class is dynamically imported.
22

33
import { Registry } from '../Registry';
4-
import { ActionConfig } from '../types/homeassistant/data/lovelace/config/action';
5-
import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card';
64
import { AreaCardConfig, StackCardConfig } from '../types/homeassistant/panels/lovelace/cards/types';
75
import { PersonCardConfig } from '../types/lovelace-mushroom/cards/person-card-config';
86
import { TemplateCardConfig } from '../types/lovelace-mushroom/cards/template-card-config';
@@ -13,9 +11,13 @@ import { logMessage, lvlError, lvlInfo } from '../utilities/debug';
1311
import { localize } from '../utilities/localize';
1412
import AbstractView from './AbstractView';
1513
import registryFilter from '../utilities/RegistryFilter';
16-
import { stackHorizontal } from '../utilities/cardStacking';
1714
import { LovelaceViewConfig } from '../types/homeassistant/data/lovelace/config/view';
1815
import { LovelaceBadgeConfig } from '../types/homeassistant/data/lovelace/config/badge';
16+
import { LovelaceSectionRawConfig } from '../types/homeassistant/data/lovelace/config/section';
17+
import { ActionConfig } from '../types/homeassistant/data/lovelace/config/action';
18+
import HeaderCard from '../cards/HeaderCard';
19+
import { stackHorizontal } from '../utilities/cardStacking';
20+
import { LovelaceCardConfig } from '../types/homeassistant/data/lovelace/config/card';
1921

2022
/**
2123
* Home View Class.
@@ -41,8 +43,8 @@ class HomeView extends AbstractView {
4143
// TODO: Move type and max_columns to the abstract class.
4244
static getDefaultConfig(): ViewConfig {
4345
return {
44-
//type: 'sections',
45-
//max_columns: 4,
46+
type: 'sections',
47+
max_columns: 3,
4648
header: {
4749
badges_position: 'top',
4850
layout: 'center',
@@ -60,38 +62,8 @@ class HomeView extends AbstractView {
6062
* The configuration includes the card configurations which are created by createCardConfigurations().
6163
*/
6264
async getView(): Promise<LovelaceViewConfig> {
63-
return {
64-
...this.baseConfiguration,
65-
badges: await this.createBadgeSection(),
66-
cards: await this.createCardConfigurations(),
67-
};
68-
}
69-
70-
/**
71-
* Create the configuration of the cards to include in the view.
72-
*
73-
* @override
74-
*/
75-
async createCardConfigurations(): Promise<LovelaceCardConfig[]> {
76-
const homeViewCards: LovelaceCardConfig[] = [];
77-
78-
let personsSection, areasSection;
79-
80-
try {
81-
[personsSection, areasSection] = await Promise.all([this.createPersonsSection(), this.createAreasSection()]);
82-
} catch (e) {
83-
logMessage(lvlError, 'Error importing created sections!', e);
84-
85-
return homeViewCards;
86-
}
87-
88-
if (personsSection) {
89-
homeViewCards.push(personsSection);
90-
}
91-
92-
// Create the greeting section.
93-
if (!Registry.strategyOptions.home_view.hidden.includes('greeting')) {
94-
homeViewCards.push({
65+
if (this.baseConfiguration.header && !Registry.strategyOptions.home_view.hidden.includes('greeting')) {
66+
this.baseConfiguration.header.card = {
9567
type: 'custom:mushroom-template-card',
9668
primary: `{% set time = now().hour %}
9769
{% if (time >= 18) %}
@@ -113,27 +85,76 @@ class HomeView extends AbstractView {
11385
hold_action: {
11486
action: 'none',
11587
} as ActionConfig,
116-
} as TemplateCardConfig);
88+
} as TemplateCardConfig;
11789
}
11890

119-
if (Registry.strategyOptions.quick_access_cards) {
120-
homeViewCards.push(...Registry.strategyOptions.quick_access_cards);
121-
}
91+
return {
92+
...this.baseConfiguration,
93+
badges: await this.createBadgeSection(),
94+
sections: await this.createSections(),
95+
};
96+
}
12297

123-
if (areasSection) {
124-
homeViewCards.push(areasSection);
125-
}
98+
/**
99+
* Create the configuration of the cards to include in the view.
100+
*
101+
* @override
102+
*/
103+
async createSections(): Promise<LovelaceSectionRawConfig[]> {
104+
const MEDIA_QUERY = {
105+
SMALL: '(max-width: 1343px)',
106+
LARGE: '(min-width: 1344px)',
107+
};
108+
const sections: LovelaceSectionRawConfig[] = [];
109+
110+
const addSection = (title: string, cards: LovelaceCardConfig[], mediaQuery?: string) => {
111+
const section: LovelaceSectionRawConfig = {
112+
type: 'grid',
113+
/*title: title,*/ // TODO: Property is deprecated.
114+
cards: cards,
115+
};
116+
117+
if (mediaQuery) {
118+
section.visibility = [
119+
{
120+
condition: 'screen',
121+
media_query: mediaQuery,
122+
},
123+
];
124+
}
125+
126+
sections.push(section);
127+
};
126128

127-
if (Registry.strategyOptions.extra_cards) {
128-
homeViewCards.push(...Registry.strategyOptions.extra_cards);
129+
try {
130+
const [personCards, areaCards] = await Promise.all([this.createPersonCards(), this.createAreaCards()]);
131+
132+
const sectionConfigurations = [
133+
['Persons', [personCards], MEDIA_QUERY.SMALL, !!personCards],
134+
['Quick Access Wide', Registry.strategyOptions.quick_access_cards, MEDIA_QUERY.LARGE, true],
135+
[
136+
'Persons and Areas',
137+
[personCards, areaCards].filter(Boolean),
138+
MEDIA_QUERY.LARGE,
139+
!!(personCards || areaCards),
140+
],
141+
['Quick Access Narrow', Registry.strategyOptions.quick_access_cards, MEDIA_QUERY.SMALL, true],
142+
['Areas', [areaCards], MEDIA_QUERY.SMALL, !!areaCards],
143+
['Extra', Registry.strategyOptions.extra_cards, undefined, true],
144+
] as const;
145+
146+
sectionConfigurations.forEach(([title, cards, mediaQuery, condition]) => {
147+
if (condition && cards.length) {
148+
addSection(title, cards as LovelaceCardConfig[], mediaQuery);
149+
return;
150+
}
151+
logMessage(lvlInfo, `Section ${title} has no entities available.`);
152+
});
153+
} catch (e) {
154+
logMessage(lvlError, 'Error importing section cards!', e);
129155
}
130156

131-
return [
132-
{
133-
type: 'vertical-stack',
134-
cards: homeViewCards,
135-
},
136-
];
157+
return sections;
137158
}
138159

139160
/**
@@ -205,10 +226,9 @@ class HomeView extends AbstractView {
205226
*
206227
* If the section is marked as hidden in the strategy option, then the section is not created.
207228
*/
208-
private async createPersonsSection(): Promise<StackCardConfig | undefined> {
229+
private async createPersonCards(): Promise<StackCardConfig | undefined> {
209230
if (Registry.strategyOptions.home_view.hidden.includes('persons')) {
210-
// The section is hidden.
211-
231+
logMessage(lvlInfo, 'Persons section is hidden.');
212232
return;
213233
}
214234

@@ -221,8 +241,13 @@ class HomeView extends AbstractView {
221241
.map((person) => new PersonCard(person).getCard())
222242
);
223243

244+
cardConfigurations.push(...cardConfigurations);
245+
224246
return {
225247
type: 'vertical-stack',
248+
grid_options: {
249+
columns: 'full',
250+
},
226251
cards: stackHorizontal(
227252
cardConfigurations,
228253
Registry.strategyOptions.home_view.stack_count['persons'] ?? Registry.strategyOptions.home_view.stack_count['_']
@@ -236,9 +261,9 @@ class HomeView extends AbstractView {
236261
* Area cards are grouped into two areas per row.
237262
* If the section is marked as hidden in the strategy option, then the section is not created.
238263
*/
239-
private async createAreasSection(): Promise<StackCardConfig | undefined> {
264+
private async createAreaCards(): Promise<StackCardConfig | undefined> {
240265
if (Registry.strategyOptions.home_view.hidden.includes('areas')) {
241-
// Areas section is hidden.
266+
logMessage(lvlInfo, 'Areas section is hidden.');
242267
return;
243268
}
244269

@@ -269,9 +294,13 @@ class HomeView extends AbstractView {
269294
);
270295
}
271296

297+
if (!Registry.strategyOptions.home_view.hidden.includes('areasTitle')) {
298+
cardConfigurations.unshift(new HeaderCard({}, { title: localize('generic.areas') }).createCard());
299+
}
300+
272301
return {
273302
type: 'vertical-stack',
274-
title: Registry.strategyOptions.home_view.hidden.includes('areasTitle') ? undefined : localize('generic.areas'),
303+
columns: 'full',
275304
cards: stackHorizontal(cardConfigurations, Registry.strategyOptions.home_view.stack_count['_'], {
276305
'custom:mushroom-template-card': Registry.strategyOptions.home_view.stack_count.areas?.[0],
277306
area: Registry.strategyOptions.home_view.stack_count.areas?.[1],

0 commit comments

Comments
 (0)