extracted list-item-components out of: DeviceList, ShutterList, TunableList, KnxGroupList

This commit is contained in:
Patrick Haßel 2024-11-28 11:06:12 +01:00
parent 120d6fffdc
commit ad130fc35e
30 changed files with 461 additions and 337 deletions

View File

@ -78,3 +78,11 @@ export function orNull<T, R>(item: T | null | undefined, map: (t: T) => R): R |
}
return map(item);
}
export function isSet(value: any) {
return value !== null && value !== undefined;
}
export function isUnset(value: any) {
return value === null || value === undefined;
}

View File

@ -1,24 +1,5 @@
<div class="deviceList tileContainer">
<div class="tileContainer deviceList">
<div class="tile" *ngFor="let device of sorted(); trackBy: Device.trackBy">
<div class="device tileInner" [ngClass]="ngClass(device)">
<div class="name">
{{ device.nameWithArea }}
</div>
<div class="actions">
<div class="action switchOn" (click)="deviceService.setState(device, true)"></div>
<div class="action switchOff" (click)="deviceService.setState(device, false)"></div>
</div>
<div class="timestamp details">
{{ device.stateProperty?.lastValueChange | relative:now }}
</div>
</div>
</div>
<app-device-tile [now]="now" [device]="device" *ngFor="let device of sorted(); trackBy: Device.trackBy"></app-device-tile>
</div>

View File

@ -1,41 +1 @@
@import "../../../config";
.deviceList {
.device {
.name {
float: left;
}
.timestamp {
clear: left;
float: left;
font-size: 80%;
}
.actions {
float: right;
.action {
float: left;
margin-left: @space;
width: 4em;
aspect-ratio: 1;
}
.switchOn {
//noinspection CssUnknownTarget
background-image: url("/switchOn.svg");
}
.switchOff {
//noinspection CssUnknownTarget
background-image: url("/switchOff.svg");
}
}
}
}

View File

@ -1,17 +1,15 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgClass, NgForOf} from '@angular/common';
import {NgForOf} from '@angular/common';
import {Device} from '../../api/Device/Device';
import {DeviceService} from '../../api/Device/device.service';
import {RelativePipe} from '../../api/common/relative.pipe';
import {Subscription, timer} from 'rxjs';
import {DeviceTileComponent} from '../device-tile/device-tile.component';
@Component({
selector: 'app-device-list',
standalone: true,
imports: [
NgForOf,
NgClass,
RelativePipe
DeviceTileComponent
],
templateUrl: './device-list.component.html',
styleUrl: './device-list.component.less'
@ -27,12 +25,6 @@ export class DeviceListComponent implements OnInit, OnDestroy {
@Input()
list: Device[] = [];
constructor(
protected readonly deviceService: DeviceService,
) {
//
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
@ -42,13 +34,6 @@ export class DeviceListComponent implements OnInit, OnDestroy {
this.subs.forEach(sub => sub.unsubscribe());
}
ngClass(device: Device) {
return {
"stateOn": device.stateProperty?.state?.value === true,
"stateOff": device.stateProperty?.state?.value === false,
};
}
sorted(): Device[] {
return this.list.sort(Device.compareByAreaThenName);
}

View File

@ -0,0 +1,20 @@
<div class="tile">
<div class="tileInner device" [ngClass]="ngClass(device)">
<div class="name">
{{ device.nameWithArea }}
</div>
<div class="actions">
<div class="action switchOn" (click)="deviceService.setState(device, true)"></div>
<div class="action switchOff" (click)="deviceService.setState(device, false)"></div>
</div>
<div class="timestamp details">
{{ device.stateProperty?.lastValueChange | relative:now }}
</div>
</div>
</div>

View File

@ -0,0 +1,37 @@
@import "../../../config";
.device {
.name {
float: left;
}
.timestamp {
clear: left;
float: left;
font-size: 80%;
}
.actions {
float: right;
.action {
float: left;
margin-left: @space;
width: 4em;
aspect-ratio: 1;
}
.switchOn {
//noinspection CssUnknownTarget
background-image: url("/switchOn.svg");
}
.switchOff {
//noinspection CssUnknownTarget
background-image: url("/switchOff.svg");
}
}
}

View File

@ -0,0 +1,38 @@
import {Component, Input} from '@angular/core';
import {RelativePipe} from "../../api/common/relative.pipe";
import {Device} from "../../api/Device/Device";
import {DeviceService} from '../../api/Device/device.service';
import {NgClass} from '@angular/common';
@Component({
selector: 'app-device-tile',
standalone: true,
imports: [
RelativePipe,
NgClass
],
templateUrl: './device-tile.component.html',
styleUrl: './device-tile.component.less'
})
export class DeviceTileComponent {
@Input()
now!: Date;
@Input()
device!: Device;
constructor(
protected readonly deviceService: DeviceService,
) {
//
}
ngClass(device: Device) {
return {
"stateOn": device.stateProperty?.state?.value === true,
"stateOff": device.stateProperty?.state?.value === false,
};
}
}

View File

@ -1,35 +1,5 @@
<div class="groupList tileContainer">
<div class="tileContainer groupList">
<div class="tile" *ngFor="let group of sorted(); trackBy: Group.trackBy">
<div class="group tileInner" [ngClass]="ngClass(group)">
<div class="name">
{{ group.name }}
</div>
<div class="details">
<div class="stackLeft address">
{{ group.address }}
</div>
<div class="stackLeft dpt">
DPT {{ group.dpt }}
</div>
<div class="stackRight state">
{{ group.state?.string || '-' }}
</div>
<div class="stackRight timestamp">
{{ group.lastValueChange | relative:now }}:
</div>
</div>
</div>
</div>
<app-knx-group-tile [now]="now" [group]="group" *ngFor="let group of sorted(); trackBy: Group.trackBy"></app-knx-group-tile>
</div>

View File

@ -1,13 +1 @@
@import "../../../config";
.groupList {
.group {
.name {
margin-bottom: @space;
}
}
}

View File

@ -1,16 +1,15 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgClass, NgForOf} from '@angular/common';
import {NgForOf} from '@angular/common';
import {Group} from '../../api/Group/Group';
import {RelativePipe} from '../../api/common/relative.pipe';
import {Subscription, timer} from 'rxjs';
import {KnxGroupTileComponent} from '../knx-group-tile/knx-group-tile.component';
@Component({
selector: 'app-knx-group-list',
standalone: true,
imports: [
NgForOf,
NgClass,
RelativePipe
KnxGroupTileComponent
],
templateUrl: './knx-group-list.component.html',
styleUrl: './knx-group-list.component.less'
@ -26,13 +25,6 @@ export class KnxGroupListComponent implements OnInit, OnDestroy {
@Input()
groupList: Group[] = [];
ngClass(group: Group) {
return {
"stateOn": group.state?.value === true,
"stateOff": group.state?.value === false,
};
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));

View File

@ -0,0 +1,31 @@
<div class="tile">
<div class="tileInner group" [ngClass]="ngClass(group)">
<div class="name">
{{ group.name }}
</div>
<div class="details">
<div class="stackLeft address">
{{ group.address }}
</div>
<div class="stackLeft dpt">
DPT {{ group.dpt }}
</div>
<div class="stackRight state">
{{ group.state?.string || '-' }}
</div>
<div class="stackRight timestamp">
{{ group.lastValueChange | relative:now }}:
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,9 @@
@import "../../../config";
.group {
.name {
margin-bottom: @space;
}
}

View File

@ -0,0 +1,33 @@
import {Component, Input} from '@angular/core';
import {NgClass} from '@angular/common';
import {RelativePipe} from '../../api/common/relative.pipe';
import {Group} from '../../api/Group/Group';
@Component({
selector: 'app-knx-group-tile',
standalone: true,
imports: [
RelativePipe,
NgClass
],
templateUrl: './knx-group-tile.component.html',
styleUrl: './knx-group-tile.component.less'
})
export class KnxGroupTileComponent {
protected readonly Group = Group;
@Input()
now!: Date;
@Input()
group!: Group;
ngClass(group: Group) {
return {
"stateOn": group.state?.value === true,
"stateOff": group.state?.value === false,
};
}
}

View File

@ -1,5 +0,0 @@
<div class="window" (click)="activate.emit(position)">
<div class="shutter" [style.height]="position + '%'">
<!-- -->
</div>
</div>

View File

@ -1,41 +1,5 @@
<div class="shutterList tileContainer">
<div class="tileContainer shutterList">
<div class="tile" *ngFor="let shutter of sorted(); trackBy: Shutter.trackBy">
<div class="shutter tileInner">
<div class="name">
{{ shutter.nameWithArea }}
</div>
<div class="icon">
<app-shutter-icon [position]="shutter.positionProperty?.state?.value"></app-shutter-icon>
</div>
<div class="timestamp details">
{{ shutter.positionProperty?.lastValueChange | relative:now }}
</div>
<div class="actions">
<div class="action">
<app-shutter-icon [position]="0" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="50" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="80" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="90" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="100" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
</div>
</div>
</div>
<app-shutter-tile [now]="now" [shutter]="shutter" *ngFor="let shutter of sorted(); trackBy: Shutter.trackBy"></app-shutter-tile>
</div>

View File

@ -1,38 +1 @@
@import "../../../config";
.shutterList {
.shutter {
.name {
float: left;
}
.icon {
clear: left;
float: left;
width: 4em;
aspect-ratio: 1;
}
.timestamp {
float: right;
font-size: 80%;
}
.actions {
clear: right;
float: right;
.action {
float: left;
margin-left: @space;
width: 3em;
aspect-ratio: 1;
}
}
}
}

View File

@ -1,18 +1,15 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgForOf} from '@angular/common';
import {Shutter} from '../../api/Shutter/Shutter';
import {ShutterService} from '../../api/Shutter/shutter.service';
import {RelativePipe} from '../../api/common/relative.pipe';
import {Subscription, timer} from 'rxjs';
import {ShutterIconComponent} from './shutter-icon/shutter-icon.component';
import {ShutterTileComponent} from '../shutter-tile/shutter-tile.component';
@Component({
selector: 'app-shutter-list',
standalone: true,
imports: [
NgForOf,
RelativePipe,
ShutterIconComponent
ShutterTileComponent
],
templateUrl: './shutter-list.component.html',
styleUrl: './shutter-list.component.less'
@ -28,12 +25,6 @@ export class ShutterListComponent implements OnInit, OnDestroy {
@Input()
list: Shutter[] = [];
constructor(
protected readonly shutterService: ShutterService,
) {
//
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));

View File

@ -0,0 +1,4 @@
<div class="window" (click)="activate.emit(position)">
<div *ngIf="isSet(position)" class="shutter" [style.height]="position + '%'"></div>
<div *ngIf="isUnset(position)" class="unknown">?</div>
</div>

View File

@ -1,6 +1,7 @@
@import "../../../../config";
.window {
position: relative;
width: 100%;
height: 100%;
background-color: lightskyblue;
@ -11,4 +12,15 @@
border-bottom: @border solid black;
}
.unknown {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1.25em;
color: red;
font-size: 260%;
text-align: center;
}
}

View File

@ -1,14 +1,23 @@
import {Component, EventEmitter, Input, Output} from '@angular/core';
import {NgIf} from '@angular/common';
import {isSet, isUnset} from '../../../api/common/validators';
@Component({
selector: 'app-shutter-icon',
standalone: true,
imports: [],
imports: [
NgIf
],
templateUrl: './shutter-icon.component.html',
styleUrl: './shutter-icon.component.less'
})
export class ShutterIconComponent {
protected readonly isUnset = isUnset;
protected readonly isSet = isSet;
@Input()
position?: number

View File

@ -0,0 +1,37 @@
<div class="tile">
<div class="tileInner shutter" [class.unknown]="isUnset(shutter.positionProperty?.state?.value)">
<div class="name">
{{ shutter.nameWithArea }}
</div>
<div class="icon">
<app-shutter-icon [position]="shutter.positionProperty?.state?.value"></app-shutter-icon>
</div>
<div class="timestamp details">
{{ shutter.positionProperty?.lastValueChange | relative:now }}
</div>
<div class="actions">
<div class="action">
<app-shutter-icon [position]="0" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="50" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="80" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="90" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
<div class="action">
<app-shutter-icon [position]="100" (activate)="shutterService.setPosition(shutter, $event)"></app-shutter-icon>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,36 @@
@import "../../../config";
.shutter {
background-color: lightgray;
border: @border solid gray !important;
.name {
float: left;
}
.icon {
clear: left;
float: left;
width: 4em;
aspect-ratio: 1;
}
.timestamp {
float: right;
font-size: 80%;
}
.actions {
clear: right;
float: right;
.action {
float: left;
margin-left: @space;
width: 3em;
aspect-ratio: 1;
}
}
}

View File

@ -0,0 +1,34 @@
import {Component, Input} from '@angular/core';
import {RelativePipe} from "../../api/common/relative.pipe";
import {Shutter} from "../../api/Shutter/Shutter";
import {ShutterService} from '../../api/Shutter/shutter.service';
import {ShutterIconComponent} from './shutter-icon/shutter-icon.component';
import {isUnset} from '../../api/common/validators';
@Component({
selector: 'app-shutter-tile',
standalone: true,
imports: [
RelativePipe,
ShutterIconComponent
],
templateUrl: './shutter-tile.component.html',
styleUrl: './shutter-tile.component.less'
})
export class ShutterTileComponent {
protected readonly isUnset = isUnset;
@Input()
now!: Date;
@Input()
shutter!: Shutter;
constructor(
protected readonly shutterService: ShutterService,
) {
//
}
}

View File

@ -1,33 +1,5 @@
<div class="tunableList tileContainer">
<div class="tileContainer tunableList">
<div *ngFor="let tunable of sorted(); trackBy: Tunable.trackBy" class="tile">
<div [ngClass]="ngClass(tunable)" class="tunable tileInner">
<div class="name">
{{ tunable.nameWithArea }}
</div>
<div class="timestamp details">
{{ tunable.stateProperty?.lastValueChange | relative:now }}
</div>
<div class="sliders">
<div class="slider" *ngIf="tunable.brightnessPropertyId !== ''">
<input type="range" [ngModel]="tunable.brightnessProperty?.state?.value" (ngModelChange)="tunableService.setBrightness(tunable, $event)">
</div>
<div class="slider" *ngIf="tunable.coldnessPropertyId !== ''">
<input type="range" [ngModel]="tunable.coldnessProperty?.state?.value" (ngModelChange)="tunableService.setColdness(tunable, $event)">
</div>
</div>
<div class="actions">
<div class="switch switchOn" (click)="tunableService.setState(tunable, true)"></div>
<div class="switch switchOff" (click)="tunableService.setState(tunable, false)"></div>
</div>
</div>
</div>
<app-tunable-tile [now]="now" [tunable]="tunable" *ngFor="let tunable of sorted(); trackBy: Tunable.trackBy"></app-tunable-tile>
</div>

View File

@ -1,63 +1 @@
@import "../../../config";
.tunableList {
.tunable {
.name {
float: left;
}
.timestamp {
float: right;
font-size: 80%;
}
.sliders {
float: left;
clear: left;
width: 60%;
overflow: visible;
padding-top: 0.4em;
.slider {
float: left;
clear: left;
margin-left: @space;
width: 100%;
overflow: visible;
input {
width: 100%;
height: 2em;
}
}
}
.actions {
float: right;
.switch {
float: left;
margin-left: @space;
width: 4em;
aspect-ratio: 1;
}
.switchOn {
//noinspection CssUnknownTarget
background-image: url("/switchOn.svg");
}
.switchOff {
//noinspection CssUnknownTarget
background-image: url("/switchOff.svg");
}
}
}
}

View File

@ -1,20 +1,17 @@
import {Component, Input, OnDestroy, OnInit} from '@angular/core';
import {NgClass, NgForOf, NgIf} from '@angular/common';
import {NgForOf} from '@angular/common';
import {Tunable} from '../../api/Tunable/Tunable';
import {TunableService} from '../../api/Tunable/tunable.service';
import {RelativePipe} from '../../api/common/relative.pipe';
import {Subscription, timer} from 'rxjs';
import {FormsModule} from '@angular/forms';
import {TunableTileComponent} from '../tunable-tile/tunable-tile.component';
@Component({
selector: 'app-tunable-list',
standalone: true,
imports: [
NgForOf,
NgClass,
RelativePipe,
FormsModule,
NgIf
TunableTileComponent
],
templateUrl: './tunable-list.component.html',
styleUrl: './tunable-list.component.less'
@ -30,12 +27,6 @@ export class TunableListComponent implements OnInit, OnDestroy {
private readonly subs: Subscription[] = [];
constructor(
protected readonly tunableService: TunableService,
) {
//
}
ngOnInit(): void {
this.now = new Date();
this.subs.push(timer(1000, 1000).subscribe(() => this.now = new Date()));
@ -45,13 +36,6 @@ export class TunableListComponent implements OnInit, OnDestroy {
this.subs.forEach(sub => sub.unsubscribe());
}
ngClass(tunable: Tunable) {
return {
"stateOn": tunable.stateProperty?.state?.value === true,
"stateOff": tunable.stateProperty?.state?.value === false,
};
}
sorted(): Tunable[] {
return this.list.sort(Tunable.compareByAreaThenName);
}

View File

@ -0,0 +1,29 @@
<div class="tile">
<div class="tileInner tunable" [ngClass]="ngClass(tunable)">
<div class="name">
{{ tunable.nameWithArea }}
</div>
<div class="timestamp details">
{{ tunable.stateProperty?.lastValueChange | relative:now }}
</div>
<div class="sliders">
<div class="slider" *ngIf="tunable.brightnessPropertyId !== ''">
<input type="range" [ngModel]="tunable.brightnessProperty?.state?.value" (ngModelChange)="tunableService.setBrightness(tunable, $event)">
</div>
<div class="slider" *ngIf="tunable.coldnessPropertyId !== ''">
<input type="range" [ngModel]="tunable.coldnessProperty?.state?.value" (ngModelChange)="tunableService.setColdness(tunable, $event)">
</div>
</div>
<div class="actions">
<div class="switch switchOn" (click)="tunableService.setState(tunable, true)"></div>
<div class="switch switchOff" (click)="tunableService.setState(tunable, false)"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,59 @@
@import "../../../config";
.tunable {
.name {
float: left;
}
.timestamp {
float: right;
font-size: 80%;
}
.sliders {
float: left;
clear: left;
width: 60%;
overflow: visible;
padding-top: 0.4em;
.slider {
float: left;
clear: left;
margin-left: @space;
width: 100%;
overflow: visible;
input {
width: 100%;
height: 2em;
}
}
}
.actions {
float: right;
.switch {
float: left;
margin-left: @space;
width: 4em;
aspect-ratio: 1;
}
.switchOn {
//noinspection CssUnknownTarget
background-image: url("/switchOn.svg");
}
.switchOff {
//noinspection CssUnknownTarget
background-image: url("/switchOff.svg");
}
}
}

View File

@ -0,0 +1,44 @@
import {Component, Input} from '@angular/core';
import {NgClass, NgIf} from "@angular/common";
import {FormsModule, ReactiveFormsModule} from "@angular/forms";
import {RelativePipe} from "../../api/common/relative.pipe";
import {Tunable} from "../../api/Tunable/Tunable";
import {TunableService} from '../../api/Tunable/tunable.service';
@Component({
selector: 'app-tunable-tile',
standalone: true,
imports: [
NgIf,
ReactiveFormsModule,
RelativePipe,
NgClass,
FormsModule
],
templateUrl: './tunable-tile.component.html',
styleUrl: './tunable-tile.component.less'
})
export class TunableTileComponent {
protected readonly Tunable = Tunable;
@Input()
now!: Date;
@Input()
tunable!: Tunable;
constructor(
protected readonly tunableService: TunableService,
) {
//
}
ngClass(tunable: Tunable) {
return {
"stateOn": tunable.stateProperty?.state?.value === true,
"stateOff": tunable.stateProperty?.state?.value === false,
};
}
}

View File

@ -55,6 +55,7 @@
.tileInner {
padding: @space;
border: @border solid lightgray;
border-radius: calc(@space / 2);
}
}