mirror of
https://github.com/jorisbertomeu/web-screensaver.git
synced 2026-04-20 00:37:40 +02:00
first
This commit is contained in:
0
src/app/pages/admin/admin.component.css
Normal file
0
src/app/pages/admin/admin.component.css
Normal file
126
src/app/pages/admin/admin.component.html
Normal file
126
src/app/pages/admin/admin.component.html
Normal file
@@ -0,0 +1,126 @@
|
||||
<div class="container">
|
||||
<div class="row mt-3 mb-3">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-body text-center">
|
||||
<a href="/" class="float-start btn btn-primary btn-sm"><i class="fa fa-arrow-left me-2"></i>Back to Dashboard</a>
|
||||
<h3 class="mb-0">Web Screensaver Admin Area</h3>
|
||||
<small class="text-muted">Version 1.0.2</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mt-3">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<i class="fa fa-screwdriver-wrench me-2"></i>Main Settings
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label class="form-label">Background Refresh Delay</label>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="fa fa-stopwatch"></i></span>
|
||||
<input [(ngModel)]="settings.refreshDelay" type="number" class="form-control" placeholder="ex: 60">
|
||||
<span class="input-group-text"> Seconds</span>
|
||||
</div>
|
||||
<label class="form-label">Native resolution (Width x Height)</label>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="fa fa-display"></i></span>
|
||||
<input [(ngModel)]="settings.resolution.width" type="number" class="form-control" placeholder="ex: 1920">
|
||||
<span class="input-group-text">x</span>
|
||||
<input [(ngModel)]="settings.resolution.height" type="number" class="form-control" placeholder="ex: 1080">
|
||||
</div>
|
||||
<hr>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input [(ngModel)]="settings.unsplash.enabled" class="form-check-input" type="checkbox" role="switch" id="useUnsplashSwitch">
|
||||
<label class="form-check-label" for="useUnsplashSwitch">Use Unsplash as Picture provider</label>
|
||||
</div>
|
||||
<ng-container *ngIf="settings.unsplash.enabled">
|
||||
<div class="row mb-3">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Access Key</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-key"></i></span>
|
||||
<input [(ngModel)]="settings.unsplash.accessKey" type="text" class="form-control" placeholder="ex: BRONXJ...">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Secret Key</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fa fa-lock"></i></span>
|
||||
<input [(ngModel)]="settings.unsplash.secretKey" type="text" class="form-control" placeholder="ex: eGyRBNKsX...">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="form-label">Unsplash Collections ID (Comma separeted)</label>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="fa fa-layer-group"></i></span>
|
||||
<input [(ngModel)]="settings.unsplash.collectionsId" type="text" class="form-control" placeholder="ex: 1806988,1427155">
|
||||
</div>
|
||||
</ng-container>
|
||||
<button [disabled]="isSaving" (click)="saveSettings()" class="btn btn-primary float-end"><i class="fa fa-save me-2"></i>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-6 mt-3">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
<img src="https://upload.wikimedia.org/wikipedia/commons/6/6e/Home_Assistant_Logo.svg" class="me-2" style="width: 24px;">HASS Configuration
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<label class="form-label">Home Assistant URL</label>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="fa fa-cog"></i></span>
|
||||
<input [(ngModel)]="settings.hass.endpoint" type="text" class="form-control" placeholder="ex: https://home.myhome.com">
|
||||
</div>
|
||||
<label class="form-label">Long-Lived Access Token</label>
|
||||
<div class="input-group mb-3">
|
||||
<span class="input-group-text"><i class="fa fa-lock"></i></span>
|
||||
<input [(ngModel)]="settings.hass.token" type="text" class="form-control" placeholder="ex: eyJhbGciOiJIUz...">
|
||||
</div>
|
||||
<button [disabled]="isSaving" (click)="saveSettings()" class="btn btn-primary float-end"><i class="fa fa-save me-2"></i>Save</button>
|
||||
</div>
|
||||
<div class="card-header border-top">
|
||||
<i class="fa fa-cubes me-2"></i>Widgets
|
||||
<button (click)="addWidget()" class="btn btn-clear float-end p-0"><i class="fa fa-circle-plus text-success"></i></button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-3" *ngFor="let w of widgets; let index = index">
|
||||
<div class="card shadow">
|
||||
<div class="card-header">
|
||||
Widget #{{index+1}}
|
||||
<button (click)="removeWidget(index)" class="btn btn-clear p-0 float-end">
|
||||
<i class="fa fa-trash text-danger"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-2">
|
||||
<label class="form-label">HASS Entity</label>
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text"><i class="fa fa-cube"></i></span>
|
||||
<input [(ngModel)]="w.entityId" type="text" class="form-control" placeholder="ex: sensor.temperature">
|
||||
</div>
|
||||
<label class="form-label">FA Icon</label>
|
||||
<div class="input-group mb-2">
|
||||
<span class="input-group-text"><i class="fa fa-icons"></i></span>
|
||||
<input [(ngModel)]="w.icon" type="text" class="form-control" placeholder="ex: fa fa-cloud-sun text-secondary">
|
||||
<span class="input-group-text"><i class="{{w.icon}}"></i></span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Prefix</label>
|
||||
<input [(ngModel)]="w.prefix" type="text" placeholder="ex: 🤷 IDK" class="form-control">
|
||||
</div>
|
||||
<div class="col-12 col-md-6">
|
||||
<label class="form-label">Suffix</label>
|
||||
<input [(ngModel)]="w.suffix" type="text" placeholder="ex: °C" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button [disabled]="isSaving" (click)="saveWidgets()" class="btn btn-primary float-end"><i class="fa fa-save me-2"></i>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
127
src/app/pages/admin/admin.component.ts
Normal file
127
src/app/pages/admin/admin.component.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-admin',
|
||||
templateUrl: './admin.component.html',
|
||||
styleUrl: './admin.component.css'
|
||||
})
|
||||
export class AdminComponent implements OnInit {
|
||||
|
||||
settings = {
|
||||
id: '',
|
||||
refreshDelay: '',
|
||||
resolution: {
|
||||
width: '',
|
||||
height: ''
|
||||
},
|
||||
unsplash: {
|
||||
enabled: false,
|
||||
accessKey: '',
|
||||
secretKey: '',
|
||||
collectionsId: ''
|
||||
},
|
||||
hass: {
|
||||
endpoint: '',
|
||||
token: ''
|
||||
}
|
||||
};
|
||||
isSaving: boolean = false;
|
||||
|
||||
widgets: Array<any> = []
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadSettings();
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
const resp = await fetch(`${environment.API.endpoint}/admin/settings`);
|
||||
const respJson = await resp.json();
|
||||
this.settings = {
|
||||
id: respJson.id || '',
|
||||
refreshDelay: respJson.refreshDelay || 60,
|
||||
resolution: {
|
||||
width: respJson?.resolution?.width || 1920,
|
||||
height: respJson?.resolution?.height || 1080
|
||||
},
|
||||
unsplash: {
|
||||
enabled: respJson?.unsplash?.enabled || false,
|
||||
accessKey: respJson?.unsplash?.accessKey || '',
|
||||
secretKey: respJson?.unsplash?.secretKey || '',
|
||||
collectionsId: respJson?.unsplash?.collectionsId ? respJson?.unsplash?.collectionsId.join(',') : ''
|
||||
},
|
||||
hass: {
|
||||
endpoint: respJson?.hass?.endpoint || '',
|
||||
token: respJson?.hass?.token || ''
|
||||
}
|
||||
};
|
||||
await this.loadWidgets();
|
||||
} catch(e) {
|
||||
console.log('Error while fetching settings', e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadWidgets() {
|
||||
try {
|
||||
const resp = await fetch(`${environment.API.endpoint}/admin/widgets/${this.settings.id}`);
|
||||
const respJson = await resp.json();
|
||||
this.widgets = respJson;
|
||||
} catch(e) {
|
||||
console.log('Error while fetching widgets', e);
|
||||
}
|
||||
}
|
||||
|
||||
addWidget() {
|
||||
this.widgets.push({
|
||||
id: null,
|
||||
icon: '',
|
||||
entityId: '',
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
});
|
||||
}
|
||||
|
||||
removeWidget(index: number) {
|
||||
if (!confirm('Are you sure?'))
|
||||
return;
|
||||
this.widgets.splice(index, 1);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
this.isSaving = true;
|
||||
try {
|
||||
await fetch(`${environment.API.endpoint}/admin/settings/${this.settings.id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(Object.assign(this.settings, {
|
||||
unsplash: Object.assign(this.settings.unsplash, {
|
||||
collectionsId: this.settings.unsplash.collectionsId.includes(',') ? this.settings.unsplash.collectionsId.split(',') : [this.settings.unsplash.collectionsId]
|
||||
})
|
||||
}))
|
||||
});
|
||||
} catch(e) {
|
||||
console.log('Error while saving settings', e);
|
||||
}
|
||||
this.isSaving = false;
|
||||
}
|
||||
|
||||
async saveWidgets() {
|
||||
this.isSaving = true;
|
||||
try {
|
||||
await fetch(`${environment.API.endpoint}/admin/widgets/${this.settings.id}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.widgets)
|
||||
});
|
||||
} catch(e) {
|
||||
console.log('Error while saving widgets', e);
|
||||
}
|
||||
this.isSaving = false;
|
||||
}
|
||||
|
||||
}
|
||||
43
src/app/pages/dashboard/dashboard.component.css
Normal file
43
src/app/pages/dashboard/dashboard.component.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.background-img {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: opacity 1s ease;
|
||||
}
|
||||
|
||||
.widget {
|
||||
position: sticky;
|
||||
z-index: 99;
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.widget-clock {
|
||||
font-size: 100px;
|
||||
}
|
||||
|
||||
.widget-bottom {
|
||||
font-size: 52px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
font-size: 48px!important;
|
||||
z-index: 99;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loader {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
}
|
||||
27
src/app/pages/dashboard/dashboard.component.html
Normal file
27
src/app/pages/dashboard/dashboard.component.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<img #bg1 class="background-img bg1"/>
|
||||
<img #bg2 class="background-img bg2" style="opacity: 0;"/>
|
||||
|
||||
<div class="loader" *ngIf="isLoading">
|
||||
<i class="fa fa-spinner fa-spin fa-4x text-primary"></i>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row mt-4">
|
||||
<div class="col-4 offset-4">
|
||||
<div class="widget widget-clock rounded-5 shadow">
|
||||
<div>{{currentClock.time}}</div>
|
||||
<div style="font-size:32px;margin-top:-25px">{{currentClock.date}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer px-3" *ngIf="bottomWidgets.length > 0">
|
||||
<div class="row mb-4">
|
||||
<div class="col" *ngFor="let w of bottomWidgets; let index = index">
|
||||
<div class="widget widget-bottom rounded-3 shadow">
|
||||
<i class="{{w.icon}} me-3 fa-fw"></i>{{w.value}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
142
src/app/pages/dashboard/dashboard.component.ts
Normal file
142
src/app/pages/dashboard/dashboard.component.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard',
|
||||
templateUrl: './dashboard.component.html',
|
||||
styleUrl: './dashboard.component.css'
|
||||
})
|
||||
export class DashboardComponent implements OnInit {
|
||||
|
||||
@ViewChild('bg1', { static: true }) bg1!: ElementRef;
|
||||
@ViewChild('bg2', { static: true }) bg2!: ElementRef;
|
||||
currentBackground: ElementRef | null = null;
|
||||
currentClock: any = {
|
||||
time: '',
|
||||
date: ''
|
||||
};
|
||||
isLoading: boolean = true;
|
||||
bottomWidgets: any[] = [];
|
||||
settings = {
|
||||
id: '',
|
||||
refreshDelay: 60,
|
||||
resolution: {
|
||||
width: '2880',
|
||||
height: '1800'
|
||||
},
|
||||
unsplash: {
|
||||
enabled: false,
|
||||
collectionsId: ''
|
||||
},
|
||||
hass: {
|
||||
endpoint: '',
|
||||
token: ''
|
||||
}
|
||||
};
|
||||
|
||||
async ngOnInit() {
|
||||
await this.loadSettings();
|
||||
this.isLoading = false;
|
||||
this.updateClock();
|
||||
this.currentBackground = this.bg1;
|
||||
this.updateBackground();
|
||||
this.updateHAWidgets();
|
||||
|
||||
setInterval(() => this.updateClock(), 750);
|
||||
setInterval(() => this.updateBackground(), this.settings.refreshDelay * 1000);
|
||||
setInterval(() => this.updateHAWidgets(), 60000);
|
||||
}
|
||||
|
||||
async loadWidgets() {
|
||||
try {
|
||||
const resp = await fetch(`${environment.API.endpoint}/admin/widgets/${this.settings.id}`);
|
||||
const respJson = await resp.json();
|
||||
this.bottomWidgets = respJson;
|
||||
} catch(e) {
|
||||
console.log('Error while fetching widgets', e);
|
||||
}
|
||||
}
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
const resp = await fetch(`${environment.API.endpoint}/admin/settings`);
|
||||
const respJson = await resp.json();
|
||||
this.settings = {
|
||||
id: respJson.id || '',
|
||||
refreshDelay: respJson.refreshDelay || 60,
|
||||
resolution: {
|
||||
width: respJson?.resolution?.width || 1920,
|
||||
height: respJson?.resolution?.height || 1080
|
||||
},
|
||||
unsplash: {
|
||||
enabled: respJson?.unsplash?.enabled || false,
|
||||
collectionsId: respJson?.unsplash?.collectionsId || ''
|
||||
},
|
||||
hass: {
|
||||
endpoint: respJson?.hass?.endpoint || '',
|
||||
token: respJson?.hass?.token || ''
|
||||
}
|
||||
};
|
||||
await this.loadWidgets();
|
||||
} catch(e) {
|
||||
console.log('Error while fetching settings', e);
|
||||
}
|
||||
}
|
||||
|
||||
updateClock() {
|
||||
const now = new Date();
|
||||
const options: any = { weekday: 'long', month: 'long', day: 'numeric' };
|
||||
let dateStr = now.toLocaleDateString('fr-FR', options);
|
||||
dateStr = dateStr.replace(/^\w| (\w)/g, match => match.toUpperCase());
|
||||
|
||||
const timeStr = now.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' });
|
||||
this.currentClock = { time: timeStr, date: dateStr };
|
||||
}
|
||||
|
||||
updateHAWidgets() {
|
||||
this.bottomWidgets.forEach(async (widget) => {
|
||||
try {
|
||||
const resp = await fetch(`${this.settings.hass.endpoint}/api/states/${widget.entityId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.settings.hass.token}`
|
||||
}
|
||||
});
|
||||
const data = await resp?.json();
|
||||
widget.value = `${widget.prefix || ''}${data.state}${widget.suffix || ''}`;
|
||||
} catch(e: any) {
|
||||
widget.value = `Error: ${e.message}`;
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
async updateBackground() {
|
||||
this.isLoading = true;
|
||||
const nextBackground = this.currentBackground === this.bg1 ? this.bg2 : this.bg1;
|
||||
let pictureResp = null;
|
||||
try {
|
||||
if (!this.settings.unsplash.enabled)
|
||||
throw new Error('Unsplash is disabled');
|
||||
pictureResp = await fetch(`${environment.API.endpoint}/photos/random/${this.settings.id}`);
|
||||
const pictureData = await pictureResp.json();
|
||||
nextBackground.nativeElement.src = `${pictureData.urls.raw}&w=${this.settings.resolution.width}&h=${this.settings.resolution.height}`;
|
||||
} catch(e) {
|
||||
console.log('Cannot request Unsplash GW API');
|
||||
nextBackground.nativeElement.src = `https://picsum.photos/${this.settings.resolution.width}/${this.settings.resolution.height}?random=${Date.now()}`;
|
||||
}
|
||||
|
||||
nextBackground.nativeElement.onload = () => {
|
||||
if (this.currentBackground === this.bg1) {
|
||||
this.bg1.nativeElement.style.opacity = "0";
|
||||
this.bg2.nativeElement.style.opacity = "1";
|
||||
this.currentBackground = this.bg2;
|
||||
} else {
|
||||
this.bg2.nativeElement.style.opacity = "0";
|
||||
this.bg1.nativeElement.style.opacity = "1";
|
||||
this.currentBackground = this.bg1;
|
||||
}
|
||||
this.isLoading = false;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user