mirror of
https://github.com/skot/ESP-Miner.git
synced 2025-03-17 13:22:53 +01:00
UI themes (#600)
* Revert "Enable PSRAM (#468)" This reverts commit e67aa1d8b72383a4d0ca75a834464e13afdacfe0. * add color themes * enable persistant themes * change back to origional red * add default theme 'dark-red'. subscribe on home component to the ThemeService
This commit is contained in:
parent
d9ee113643
commit
188336c1b6
@ -19,6 +19,7 @@ SRCS
|
||||
"lv_font_portfolio-6x8.c"
|
||||
"logo.c"
|
||||
"./http_server/http_server.c"
|
||||
"./http_server/theme_api.c"
|
||||
"./self_test/self_test.c"
|
||||
"./tasks/stratum_task.c"
|
||||
"./tasks/create_jobs_task.c"
|
||||
|
@ -18,6 +18,7 @@ import { LogsComponent } from './components/logs/logs.component';
|
||||
import { NetworkComponent } from './components/network/network.component';
|
||||
import { SettingsComponent } from './components/settings/settings.component';
|
||||
import { SwarmComponent } from './components/swarm/swarm.component';
|
||||
import { ThemeConfigComponent } from './components/settings/theme-config.component';
|
||||
import { AppLayoutModule } from './layout/app.layout.module';
|
||||
import { ANSIPipe } from './pipes/ansi.pipe';
|
||||
import { DateAgoPipe } from './pipes/date-ago.pipe';
|
||||
@ -45,7 +46,8 @@ const components = [
|
||||
DateAgoPipe,
|
||||
SwarmComponent,
|
||||
SettingsComponent,
|
||||
HashSuffixPipe
|
||||
HashSuffixPipe,
|
||||
ThemeConfigComponent
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
|
@ -2,6 +2,7 @@ import { Component } from '@angular/core';
|
||||
import { interval, map, Observable, shareReplay, startWith, switchMap, tap } from 'rxjs';
|
||||
import { HashSuffixPipe } from 'src/app/pipes/hash-suffix.pipe';
|
||||
import { SystemService } from 'src/app/services/system.service';
|
||||
import { ThemeService } from 'src/app/services/theme.service';
|
||||
import { eASICModel } from 'src/models/enum/eASICModel';
|
||||
import { ISystemInfo } from 'src/models/ISystemInfo';
|
||||
|
||||
@ -12,11 +13,10 @@ import { ISystemInfo } from 'src/models/ISystemInfo';
|
||||
})
|
||||
export class HomeComponent {
|
||||
|
||||
public info$: Observable<ISystemInfo>;
|
||||
|
||||
public quickLink$: Observable<string | undefined>;
|
||||
public fallbackQuickLink$: Observable<string | undefined>;
|
||||
public expectedHashRate$: Observable<number | undefined>;
|
||||
public info$!: Observable<ISystemInfo>;
|
||||
public quickLink$!: Observable<string | undefined>;
|
||||
public fallbackQuickLink$!: Observable<string | undefined>;
|
||||
public expectedHashRate$!: Observable<number | undefined>;
|
||||
|
||||
|
||||
public chartOptions: any;
|
||||
@ -31,9 +31,50 @@ export class HomeComponent {
|
||||
public maxFrequency: number = 800;
|
||||
|
||||
constructor(
|
||||
private systemService: SystemService
|
||||
private systemService: SystemService,
|
||||
private themeService: ThemeService
|
||||
) {
|
||||
this.initializeChart();
|
||||
|
||||
// Subscribe to theme changes
|
||||
this.themeService.getThemeSettings().subscribe(() => {
|
||||
this.updateChartColors();
|
||||
});
|
||||
}
|
||||
|
||||
private updateChartColors() {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
|
||||
const surfaceBorder = documentStyle.getPropertyValue('--surface-border');
|
||||
const primaryColor = documentStyle.getPropertyValue('--primary-color');
|
||||
|
||||
// Update chart colors
|
||||
if (this.chartData && this.chartData.datasets) {
|
||||
this.chartData.datasets[0].backgroundColor = primaryColor + '30';
|
||||
this.chartData.datasets[0].borderColor = primaryColor;
|
||||
this.chartData.datasets[1].backgroundColor = primaryColor + '30';
|
||||
this.chartData.datasets[1].borderColor = primaryColor + '60';
|
||||
this.chartData.datasets[2].backgroundColor = textColorSecondary;
|
||||
this.chartData.datasets[2].borderColor = textColorSecondary;
|
||||
}
|
||||
|
||||
// Update chart options
|
||||
if (this.chartOptions) {
|
||||
this.chartOptions.plugins.legend.labels.color = textColor;
|
||||
this.chartOptions.scales.x.ticks.color = textColorSecondary;
|
||||
this.chartOptions.scales.x.grid.color = surfaceBorder;
|
||||
this.chartOptions.scales.y.ticks.color = textColorSecondary;
|
||||
this.chartOptions.scales.y.grid.color = surfaceBorder;
|
||||
this.chartOptions.scales.y2.ticks.color = textColorSecondary;
|
||||
this.chartOptions.scales.y2.grid.color = surfaceBorder;
|
||||
}
|
||||
|
||||
// Force chart update
|
||||
this.chartData = { ...this.chartData };
|
||||
}
|
||||
|
||||
private initializeChart() {
|
||||
const documentStyle = getComputedStyle(document.documentElement);
|
||||
const textColor = documentStyle.getPropertyValue('--text-color');
|
||||
const textColorSecondary = documentStyle.getPropertyValue('--text-color-secondary');
|
||||
@ -243,4 +284,3 @@ export class HomeComponent {
|
||||
return stratumURL.startsWith('http') ? stratumURL : `http://${stratumURL}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,21 +1,24 @@
|
||||
<div class="card">
|
||||
<h2>Settings</h2>
|
||||
|
||||
<app-edit></app-edit>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
<app-theme-config></app-theme-config>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="col-12 lg:col-6 xl:col-4">
|
||||
<div class="card" *ngIf="checkLatestRelease == false">
|
||||
<h5>Current Version: {{(info$ | async)?.version}}</h5>
|
||||
<h5>Current Version: {{(info$ | async)?.version}}</h5>
|
||||
<h2>Latest Release: <p-button (onClick)="checkLatestRelease = true">Check</p-button></h2>
|
||||
<small>Clicking this button will connect to GitHub to check for recent updates</small>
|
||||
</div>
|
||||
<div class="card" *ngIf="checkLatestRelease == true">
|
||||
<ng-container *ngIf="latestRelease$ | async as latestRelease">
|
||||
<h5>Current Version: {{(info$ | async)?.version}}</h5>
|
||||
<h5>Current Version: {{(info$ | async)?.version}}</h5>
|
||||
<h2>Latest Release: {{latestRelease.name}}</h2>
|
||||
|
||||
<div *ngFor="let asset of latestRelease.assets">
|
||||
|
@ -0,0 +1,230 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { LayoutService } from '../../layout/service/app.layout.service';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
|
||||
interface ThemeOption {
|
||||
name: string;
|
||||
primaryColor: string;
|
||||
accentColors: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-theme-config',
|
||||
template: `
|
||||
<div class="card">
|
||||
<h5>Theme Configuration</h5>
|
||||
<div class="grid">
|
||||
<div class="col-12">
|
||||
<h6>Color Scheme</h6>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex align-items-center">
|
||||
<p-radioButton name="colorScheme" [value]="'dark'" [(ngModel)]="selectedScheme"
|
||||
(onClick)="changeColorScheme('dark')" inputId="dark"></p-radioButton>
|
||||
<label for="dark" class="ml-2">Dark</label>
|
||||
</div>
|
||||
<div class="flex align-items-center">
|
||||
<p-radioButton name="colorScheme" [value]="'light'" [(ngModel)]="selectedScheme"
|
||||
(onClick)="changeColorScheme('light')" inputId="light"></p-radioButton>
|
||||
<label for="light" class="ml-2">Light</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<h6>Theme Colors</h6>
|
||||
<div class="grid">
|
||||
<div *ngFor="let theme of themes" class="col-3">
|
||||
<button pButton [class]="'p-button-rounded p-button-text color-dot'"
|
||||
[style.backgroundColor]="theme.primaryColor"
|
||||
style="width: 2rem; height: 2rem; border: none;"
|
||||
(click)="changeTheme(theme)">
|
||||
</button>
|
||||
<div class="text-sm mt-1">{{theme.name}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class ThemeConfigComponent implements OnInit {
|
||||
selectedScheme: string;
|
||||
themes: ThemeOption[] = [
|
||||
{
|
||||
name: 'Red',
|
||||
primaryColor: '#F80421',
|
||||
accentColors: {
|
||||
'--primary-color': '#F80421',
|
||||
'--primary-color-text': '#ffffff',
|
||||
'--highlight-bg': '#F80421',
|
||||
'--highlight-text-color': '#ffffff',
|
||||
'--focus-ring': '0 0 0 0.2rem rgba(255,64,50,0.2)',
|
||||
// PrimeNG Slider
|
||||
'--slider-bg': '#dee2e6',
|
||||
'--slider-range-bg': '#F80421',
|
||||
'--slider-handle-bg': '#F80421',
|
||||
// Progress Bar
|
||||
'--progressbar-bg': '#dee2e6',
|
||||
'--progressbar-value-bg': '#F80421',
|
||||
// PrimeNG Checkbox
|
||||
'--checkbox-border': '#F80421',
|
||||
'--checkbox-bg': '#F80421',
|
||||
'--checkbox-hover-bg': '#e63c2e',
|
||||
// PrimeNG Button
|
||||
'--button-bg': '#F80421',
|
||||
'--button-hover-bg': '#e63c2e',
|
||||
'--button-focus-shadow': '0 0 0 2px #ffffff, 0 0 0 4px #F80421',
|
||||
// Toggle button
|
||||
'--togglebutton-bg': '#F80421',
|
||||
'--togglebutton-border': '1px solid #F80421',
|
||||
'--togglebutton-hover-bg': '#e63c2e',
|
||||
'--togglebutton-hover-border': '1px solid #e63c2e',
|
||||
'--togglebutton-text-color': '#ffffff'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Blue',
|
||||
primaryColor: '#2196f3',
|
||||
accentColors: {
|
||||
'--primary-color': '#2196f3',
|
||||
'--primary-color-text': '#ffffff',
|
||||
'--highlight-bg': '#2196f3',
|
||||
'--highlight-text-color': '#ffffff',
|
||||
'--focus-ring': '0 0 0 0.2rem rgba(33,150,243,0.2)',
|
||||
// PrimeNG Slider
|
||||
'--slider-bg': '#dee2e6',
|
||||
'--slider-range-bg': '#2196f3',
|
||||
'--slider-handle-bg': '#2196f3',
|
||||
// Progress Bar
|
||||
'--progressbar-bg': '#dee2e6',
|
||||
'--progressbar-value-bg': '#2196f3',
|
||||
// PrimeNG Checkbox
|
||||
'--checkbox-border': '#2196f3',
|
||||
'--checkbox-bg': '#2196f3',
|
||||
'--checkbox-hover-bg': '#1e88e5',
|
||||
// PrimeNG Button
|
||||
'--button-bg': '#2196f3',
|
||||
'--button-hover-bg': '#1e88e5',
|
||||
'--button-focus-shadow': '0 0 0 2px #ffffff, 0 0 0 4px #2196f3',
|
||||
// Toggle button
|
||||
'--togglebutton-bg': '#2196f3',
|
||||
'--togglebutton-border': '1px solid #2196f3',
|
||||
'--togglebutton-hover-bg': '#1e88e5',
|
||||
'--togglebutton-hover-border': '1px solid #1e88e5',
|
||||
'--togglebutton-text-color': '#ffffff'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Green',
|
||||
primaryColor: '#4caf50',
|
||||
accentColors: {
|
||||
'--primary-color': '#4caf50',
|
||||
'--primary-color-text': '#ffffff',
|
||||
'--highlight-bg': '#4caf50',
|
||||
'--highlight-text-color': '#ffffff',
|
||||
'--focus-ring': '0 0 0 0.2rem rgba(76,175,80,0.2)',
|
||||
// PrimeNG Slider
|
||||
'--slider-bg': '#dee2e6',
|
||||
'--slider-range-bg': '#4caf50',
|
||||
'--slider-handle-bg': '#4caf50',
|
||||
// Progress Bar
|
||||
'--progressbar-bg': '#dee2e6',
|
||||
'--progressbar-value-bg': '#4caf50',
|
||||
// PrimeNG Checkbox
|
||||
'--checkbox-border': '#4caf50',
|
||||
'--checkbox-bg': '#4caf50',
|
||||
'--checkbox-hover-bg': '#43a047',
|
||||
// PrimeNG Button
|
||||
'--button-bg': '#4caf50',
|
||||
'--button-hover-bg': '#43a047',
|
||||
'--button-focus-shadow': '0 0 0 2px #ffffff, 0 0 0 4px #4caf50',
|
||||
// Toggle button
|
||||
'--togglebutton-bg': '#4caf50',
|
||||
'--togglebutton-border': '1px solid #4caf50',
|
||||
'--togglebutton-hover-bg': '#43a047',
|
||||
'--togglebutton-hover-border': '1px solid #43a047',
|
||||
'--togglebutton-text-color': '#ffffff'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Purple',
|
||||
primaryColor: '#9c27b0',
|
||||
accentColors: {
|
||||
'--primary-color': '#9c27b0',
|
||||
'--primary-color-text': '#ffffff',
|
||||
'--highlight-bg': '#9c27b0',
|
||||
'--highlight-text-color': '#ffffff',
|
||||
'--focus-ring': '0 0 0 0.2rem rgba(156,39,176,0.2)',
|
||||
// PrimeNG Slider
|
||||
'--slider-bg': '#dee2e6',
|
||||
'--slider-range-bg': '#9c27b0',
|
||||
'--slider-handle-bg': '#9c27b0',
|
||||
// Progress Bar
|
||||
'--progressbar-bg': '#dee2e6',
|
||||
'--progressbar-value-bg': '#9c27b0',
|
||||
// PrimeNG Checkbox
|
||||
'--checkbox-border': '#9c27b0',
|
||||
'--checkbox-bg': '#9c27b0',
|
||||
'--checkbox-hover-bg': '#8e24aa',
|
||||
// PrimeNG Button
|
||||
'--button-bg': '#9c27b0',
|
||||
'--button-hover-bg': '#8e24aa',
|
||||
'--button-focus-shadow': '0 0 0 2px #ffffff, 0 0 0 4px #9c27b0',
|
||||
// Toggle button
|
||||
'--togglebutton-bg': '#9c27b0',
|
||||
'--togglebutton-border': '1px solid #9c27b0',
|
||||
'--togglebutton-hover-bg': '#8e24aa',
|
||||
'--togglebutton-hover-border': '1px solid #8e24aa',
|
||||
'--togglebutton-text-color': '#ffffff'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
constructor(
|
||||
public layoutService: LayoutService,
|
||||
private themeService: ThemeService
|
||||
) {
|
||||
this.selectedScheme = this.layoutService.config().colorScheme;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
// Load saved theme settings from NVS
|
||||
this.themeService.getThemeSettings().subscribe(
|
||||
settings => {
|
||||
if (settings && settings.accentColors) {
|
||||
this.applyThemeColors(settings.accentColors);
|
||||
}
|
||||
},
|
||||
error => console.error('Error loading theme settings:', error)
|
||||
);
|
||||
}
|
||||
|
||||
private applyThemeColors(colors: { [key: string]: string }) {
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
changeColorScheme(scheme: string) {
|
||||
this.selectedScheme = scheme;
|
||||
const config = { ...this.layoutService.config() };
|
||||
config.colorScheme = scheme;
|
||||
this.layoutService.config.set(config);
|
||||
}
|
||||
|
||||
changeTheme(theme: ThemeOption) {
|
||||
// Update CSS variables
|
||||
this.applyThemeColors(theme.accentColors);
|
||||
// Save theme settings to NVS
|
||||
this.themeService.saveThemeSettings({
|
||||
colorScheme: this.selectedScheme,
|
||||
theme: this.layoutService.config().theme,
|
||||
accentColors: theme.accentColors
|
||||
}).subscribe(
|
||||
() => {},
|
||||
error => console.error('Error saving theme settings:', error)
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { Injectable, effect, signal } from '@angular/core';
|
||||
import { Subject } from 'rxjs';
|
||||
import { ThemeService } from '../../services/theme.service';
|
||||
|
||||
export interface AppConfig {
|
||||
inputStyle: string;
|
||||
@ -23,12 +24,48 @@ interface LayoutState {
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LayoutService {
|
||||
private darkTheme = {
|
||||
'--surface-a': '#0B1219', // Darker navy
|
||||
'--surface-b': '#070D17', // Very dark navy (from image)
|
||||
'--surface-c': 'rgba(255,255,255,0.03)',
|
||||
'--surface-d': '#1A2632', // Slightly lighter navy
|
||||
'--surface-e': '#0B1219',
|
||||
'--surface-f': '#0B1219',
|
||||
'--surface-ground': '#070D17',
|
||||
'--surface-section': '#070D17',
|
||||
'--surface-card': '#0B1219',
|
||||
'--surface-overlay': '#0B1219',
|
||||
'--surface-border': '#1A2632',
|
||||
'--surface-hover': 'rgba(255,255,255,0.03)',
|
||||
'--text-color': 'rgba(255, 255, 255, 0.87)',
|
||||
'--text-color-secondary': 'rgba(255, 255, 255, 0.6)',
|
||||
'--maskbg': 'rgba(0,0,0,0.4)'
|
||||
};
|
||||
|
||||
private lightTheme = {
|
||||
'--surface-a': '#1a2632', // Lighter navy for main background
|
||||
'--surface-b': '#243447', // Medium navy for secondary background
|
||||
'--surface-c': 'rgba(255,255,255,0.03)',
|
||||
'--surface-d': '#2f4562', // Light navy for borders
|
||||
'--surface-e': '#1a2632',
|
||||
'--surface-f': '#1a2632',
|
||||
'--surface-ground': '#243447',
|
||||
'--surface-section': '#1a2632',
|
||||
'--surface-card': '#1a2632',
|
||||
'--surface-overlay': '#1a2632',
|
||||
'--surface-border': '#2f4562',
|
||||
'--surface-hover': 'rgba(255,255,255,0.03)',
|
||||
'--text-color': 'rgba(255, 255, 255, 0.9)', // Slightly brighter text
|
||||
'--text-color-secondary': 'rgba(255, 255, 255, 0.7)', // Brighter secondary text
|
||||
'--maskbg': 'rgba(0,0,0,0.2)'
|
||||
};
|
||||
|
||||
_config: AppConfig = {
|
||||
ripple: false,
|
||||
inputStyle: 'outlined',
|
||||
menuMode: 'static',
|
||||
colorScheme: 'light',
|
||||
theme: 'lara-light-indigo',
|
||||
colorScheme: 'dark',
|
||||
theme: 'dark',
|
||||
scale: 14,
|
||||
};
|
||||
|
||||
@ -44,31 +81,78 @@ export class LayoutService {
|
||||
};
|
||||
|
||||
private configUpdate = new Subject<AppConfig>();
|
||||
|
||||
private overlayOpen = new Subject<any>();
|
||||
|
||||
configUpdate$ = this.configUpdate.asObservable();
|
||||
|
||||
overlayOpen$ = this.overlayOpen.asObservable();
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
const config = this.config();
|
||||
if (this.updateStyle(config)) {
|
||||
constructor(private themeService: ThemeService) {
|
||||
// Load saved theme settings from NVS
|
||||
this.themeService.getThemeSettings().subscribe(
|
||||
settings => {
|
||||
if (settings) {
|
||||
this._config = {
|
||||
...this._config,
|
||||
colorScheme: settings.colorScheme,
|
||||
theme: settings.theme
|
||||
};
|
||||
// Apply accent colors if they exist
|
||||
if (settings.accentColors) {
|
||||
Object.entries(settings.accentColors).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Save default red dark theme if no settings exist
|
||||
this.themeService.saveThemeSettings({
|
||||
colorScheme: 'dark',
|
||||
theme: 'dark',
|
||||
accentColors: {
|
||||
'--primary-color': '#F80421',
|
||||
'--primary-color-text': '#ffffff',
|
||||
'--highlight-bg': '#F80421',
|
||||
'--highlight-text-color': '#ffffff',
|
||||
'--focus-ring': '0 0 0 0.2rem rgba(248,4,33,0.2)',
|
||||
'--slider-bg': '#dee2e6',
|
||||
'--slider-range-bg': '#F80421',
|
||||
'--slider-handle-bg': '#F80421',
|
||||
'--progressbar-bg': '#dee2e6',
|
||||
'--progressbar-value-bg': '#F80421',
|
||||
'--checkbox-border': '#F80421',
|
||||
'--checkbox-bg': '#F80421',
|
||||
'--checkbox-hover-bg': '#df031d',
|
||||
'--button-bg': '#F80421',
|
||||
'--button-hover-bg': '#df031d',
|
||||
'--button-focus-shadow': '0 0 0 2px #ffffff, 0 0 0 4px #F80421',
|
||||
'--togglebutton-bg': '#F80421',
|
||||
'--togglebutton-border': '1px solid #F80421',
|
||||
'--togglebutton-hover-bg': '#df031d',
|
||||
'--togglebutton-hover-border': '1px solid #df031d',
|
||||
'--togglebutton-text-color': '#ffffff'
|
||||
}
|
||||
}).subscribe();
|
||||
}
|
||||
// Update signal with config
|
||||
this.config.set(this._config);
|
||||
// Apply initial theme
|
||||
this.changeTheme();
|
||||
},
|
||||
error => {
|
||||
console.error('Error loading theme settings:', error);
|
||||
// Use default theme on error
|
||||
this.config.set(this._config);
|
||||
this.changeTheme();
|
||||
}
|
||||
);
|
||||
|
||||
effect(() => {
|
||||
const config = this.config();
|
||||
this.changeTheme();
|
||||
this.changeScale(config.scale);
|
||||
this.onConfigUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
updateStyle(config: AppConfig) {
|
||||
return (
|
||||
config.theme !== this._config.theme ||
|
||||
config.colorScheme !== this._config.colorScheme
|
||||
);
|
||||
}
|
||||
|
||||
onMenuToggle() {
|
||||
if (this.isOverlay()) {
|
||||
this.state.overlayMenuActive = !this.state.overlayMenuActive;
|
||||
@ -116,41 +200,38 @@ export class LayoutService {
|
||||
onConfigUpdate() {
|
||||
this._config = { ...this.config() };
|
||||
this.configUpdate.next(this.config());
|
||||
// Save theme settings to NVS
|
||||
this.themeService.saveThemeSettings({
|
||||
colorScheme: this._config.colorScheme,
|
||||
theme: this._config.theme
|
||||
}).subscribe(
|
||||
() => {},
|
||||
error => console.error('Error saving theme settings:', error)
|
||||
);
|
||||
// Apply theme changes immediately
|
||||
this.changeTheme();
|
||||
}
|
||||
|
||||
changeTheme() {
|
||||
const config = this.config();
|
||||
const themeLink = <HTMLLinkElement>document.getElementById('theme-css');
|
||||
const themeLinkHref = themeLink.getAttribute('href')!;
|
||||
const newHref = themeLinkHref
|
||||
.split('/')
|
||||
.map((el) =>
|
||||
el == this._config.theme
|
||||
? (el = config.theme)
|
||||
: el == `theme-${this._config.colorScheme}`
|
||||
? (el = `theme-${config.colorScheme}`)
|
||||
: el
|
||||
)
|
||||
.join('/');
|
||||
|
||||
this.replaceThemeLink(newHref);
|
||||
}
|
||||
replaceThemeLink(href: string) {
|
||||
const id = 'theme-css';
|
||||
let themeLink = <HTMLLinkElement>document.getElementById(id);
|
||||
const cloneLinkElement = <HTMLLinkElement>themeLink.cloneNode(true);
|
||||
|
||||
cloneLinkElement.setAttribute('href', href);
|
||||
cloneLinkElement.setAttribute('id', id + '-clone');
|
||||
|
||||
themeLink.parentNode!.insertBefore(
|
||||
cloneLinkElement,
|
||||
themeLink.nextSibling
|
||||
);
|
||||
cloneLinkElement.addEventListener('load', () => {
|
||||
themeLink.remove();
|
||||
cloneLinkElement.setAttribute('id', id);
|
||||
// Apply light/dark theme variables
|
||||
const themeVars = config.colorScheme === 'light' ? this.lightTheme : this.darkTheme;
|
||||
Object.entries(themeVars).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
});
|
||||
|
||||
// Load theme settings from NVS
|
||||
this.themeService.getThemeSettings().subscribe(
|
||||
settings => {
|
||||
if (settings && settings.accentColors) {
|
||||
Object.entries(settings.accentColors).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(key, value);
|
||||
});
|
||||
}
|
||||
},
|
||||
error => console.error('Error loading accent colors:', error)
|
||||
);
|
||||
}
|
||||
|
||||
changeScale(value: number) {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RadioButtonModule } from 'primeng/radiobutton';
|
||||
import { ButtonModule } from 'primeng/button';
|
||||
import { ChartModule } from 'primeng/chart';
|
||||
import { CheckboxModule } from 'primeng/checkbox';
|
||||
@ -22,7 +23,8 @@ const primeNgModules = [
|
||||
KnobModule,
|
||||
ChartModule,
|
||||
InputGroupModule,
|
||||
InputGroupAddonModule
|
||||
InputGroupAddonModule,
|
||||
RadioButtonModule
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
@ -33,4 +35,4 @@ const primeNgModules = [
|
||||
...primeNgModules
|
||||
],
|
||||
})
|
||||
export class PrimeNGModule { }
|
||||
export class PrimeNGModule { }
|
||||
|
28
main/http_server/axe-os/src/app/services/theme.service.ts
Normal file
28
main/http_server/axe-os/src/app/services/theme.service.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface ThemeSettings {
|
||||
colorScheme: string;
|
||||
theme: string;
|
||||
accentColors?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ThemeService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
// Get theme settings from NVS storage
|
||||
getThemeSettings(): Observable<ThemeSettings> {
|
||||
return this.http.get<ThemeSettings>('/api/theme');
|
||||
}
|
||||
|
||||
// Save theme settings to NVS storage
|
||||
saveThemeSettings(settings: ThemeSettings): Observable<void> {
|
||||
return this.http.post<void>('/api/theme', settings);
|
||||
}
|
||||
}
|
@ -36,4 +36,128 @@ p-chart>div {
|
||||
&.chart {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* PrimeNG Component Theme Overrides */
|
||||
.p-slider {
|
||||
.p-slider-range {
|
||||
background: var(--slider-range-bg);
|
||||
}
|
||||
.p-slider-handle {
|
||||
background: var(--slider-handle-bg);
|
||||
border: 2px solid var(--slider-handle-bg);
|
||||
}
|
||||
}
|
||||
|
||||
.p-checkbox {
|
||||
.p-checkbox-box {
|
||||
&.p-highlight {
|
||||
border-color: var(--checkbox-border);
|
||||
background: var(--checkbox-bg);
|
||||
|
||||
&:hover {
|
||||
background: var(--checkbox-hover-bg);
|
||||
border-color: var(--checkbox-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-button {
|
||||
background: var(--button-bg);
|
||||
border: 1px solid var(--button-bg);
|
||||
|
||||
&:enabled:hover {
|
||||
background: var(--button-hover-bg);
|
||||
border-color: var(--button-hover-bg);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: var(--button-focus-shadow);
|
||||
}
|
||||
}
|
||||
|
||||
.p-button.p-button-text.color-dot {
|
||||
&:enabled:hover {
|
||||
background: var(--button-bg);
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
&:enabled:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove focus ring from theme color buttons
|
||||
button.color-dot {
|
||||
&:focus {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.p-togglebutton {
|
||||
&.p-highlight {
|
||||
background: var(--togglebutton-bg);
|
||||
border-color: var(--togglebutton-bg);
|
||||
|
||||
&:hover {
|
||||
background: var(--togglebutton-hover-bg);
|
||||
border-color: var(--togglebutton-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.p-inputtext {
|
||||
background: transparent !important;
|
||||
transition: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
|
||||
&:enabled:hover,
|
||||
&:enabled:focus {
|
||||
border-color: var(--togglebutton-hover-bg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Progress Bar Theme Override */
|
||||
.p-progressbar {
|
||||
background: var(--progressbar-bg);
|
||||
|
||||
.p-progressbar-value {
|
||||
background: var(--progressbar-value-bg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dropdown Theme*/
|
||||
.p-dropdown {
|
||||
background: transparent !important;
|
||||
transition: none !important;
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.p-dropdown-open {
|
||||
border-color: var(--togglebutton-hover-bg) !important;
|
||||
}
|
||||
|
||||
.p-dropdown-panel {
|
||||
background: var(--surface-overlay) !important;
|
||||
|
||||
.p-dropdown-items {
|
||||
.p-dropdown-item {
|
||||
&:hover {
|
||||
background: var(--highlight-bg) !important;
|
||||
color: var(--highlight-text-color) !important;
|
||||
}
|
||||
|
||||
&.p-highlight {
|
||||
background: var(--highlight-bg) !important;
|
||||
color: var(--highlight-text-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#include "http_server.h"
|
||||
#include "recovery_page.h"
|
||||
#include "theme_api.h" // Add theme API include
|
||||
#include "cJSON.h"
|
||||
#include "esp_chip_info.h"
|
||||
#include "esp_http_server.h"
|
||||
@ -712,6 +713,9 @@ esp_err_t start_rest_server(void * pvParameters)
|
||||
httpd_uri_t recovery_explicit_get_uri = {
|
||||
.uri = "/recovery", .method = HTTP_GET, .handler = rest_recovery_handler, .user_ctx = rest_context};
|
||||
httpd_register_uri_handler(server, &recovery_explicit_get_uri);
|
||||
|
||||
// Register theme API endpoints
|
||||
ESP_ERROR_CHECK(register_theme_api_endpoints(server, rest_context));
|
||||
|
||||
/* URI handler for fetching system info */
|
||||
httpd_uri_t system_info_get_uri = {
|
||||
|
149
main/http_server/theme_api.c
Normal file
149
main/http_server/theme_api.c
Normal file
@ -0,0 +1,149 @@
|
||||
#include "theme_api.h"
|
||||
#include "esp_http_server.h"
|
||||
#include "esp_log.h"
|
||||
#include "nvs_config.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
static const char *TAG = "theme_api";
|
||||
|
||||
// Helper function to set CORS headers
|
||||
static esp_err_t set_cors_headers(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Origin", "*");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||
httpd_resp_set_hdr(req, "Access-Control-Allow-Headers", "Content-Type");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// Handle OPTIONS requests for CORS
|
||||
static esp_err_t theme_options_handler(httpd_req_t *req)
|
||||
{
|
||||
set_cors_headers(req);
|
||||
httpd_resp_send(req, NULL, 0);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// GET /api/theme handler
|
||||
static esp_err_t theme_get_handler(httpd_req_t *req)
|
||||
{
|
||||
httpd_resp_set_type(req, "application/json");
|
||||
set_cors_headers(req);
|
||||
|
||||
char *scheme = nvs_config_get_string(NVS_CONFIG_THEME_SCHEME, "dark");
|
||||
char *name = nvs_config_get_string(NVS_CONFIG_THEME_NAME, "dark");
|
||||
char *colors = nvs_config_get_string(NVS_CONFIG_THEME_COLORS,
|
||||
"{"
|
||||
"\"--primary-color\":\"#F80421\","
|
||||
"\"--primary-color-text\":\"#ffffff\","
|
||||
"\"--highlight-bg\":\"#F80421\","
|
||||
"\"--highlight-text-color\":\"#ffffff\","
|
||||
"\"--focus-ring\":\"0 0 0 0.2rem rgba(248,4,33,0.2)\","
|
||||
"\"--slider-bg\":\"#dee2e6\","
|
||||
"\"--slider-range-bg\":\"#F80421\","
|
||||
"\"--slider-handle-bg\":\"#F80421\","
|
||||
"\"--progressbar-bg\":\"#dee2e6\","
|
||||
"\"--progressbar-value-bg\":\"#F80421\","
|
||||
"\"--checkbox-border\":\"#F80421\","
|
||||
"\"--checkbox-bg\":\"#F80421\","
|
||||
"\"--checkbox-hover-bg\":\"#df031d\","
|
||||
"\"--button-bg\":\"#F80421\","
|
||||
"\"--button-hover-bg\":\"#df031d\","
|
||||
"\"--button-focus-shadow\":\"0 0 0 2px #ffffff, 0 0 0 4px #F80421\","
|
||||
"\"--togglebutton-bg\":\"#F80421\","
|
||||
"\"--togglebutton-border\":\"1px solid #F80421\","
|
||||
"\"--togglebutton-hover-bg\":\"#df031d\","
|
||||
"\"--togglebutton-hover-border\":\"1px solid #df031d\","
|
||||
"\"--togglebutton-text-color\":\"#ffffff\""
|
||||
"}"
|
||||
);
|
||||
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(root, "colorScheme", scheme);
|
||||
cJSON_AddStringToObject(root, "theme", name);
|
||||
|
||||
// Parse stored colors JSON string
|
||||
cJSON *colors_json = cJSON_Parse(colors);
|
||||
if (colors_json) {
|
||||
cJSON_AddItemToObject(root, "accentColors", colors_json);
|
||||
}
|
||||
|
||||
const char *response = cJSON_Print(root);
|
||||
httpd_resp_sendstr(req, response);
|
||||
|
||||
free(scheme);
|
||||
free(name);
|
||||
free(colors);
|
||||
free((char *)response);
|
||||
cJSON_Delete(root);
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
// POST /api/theme handler
|
||||
static esp_err_t theme_post_handler(httpd_req_t *req)
|
||||
{
|
||||
set_cors_headers(req);
|
||||
|
||||
// Read POST data
|
||||
char content[1024];
|
||||
int ret = httpd_req_recv(req, content, sizeof(content) - 1);
|
||||
if (ret <= 0) {
|
||||
httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read request");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
content[ret] = '\0';
|
||||
|
||||
cJSON *root = cJSON_Parse(content);
|
||||
if (!root) {
|
||||
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Invalid JSON");
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
// Update theme settings
|
||||
cJSON *item;
|
||||
if ((item = cJSON_GetObjectItem(root, "colorScheme")) != NULL) {
|
||||
nvs_config_set_string(NVS_CONFIG_THEME_SCHEME, item->valuestring);
|
||||
}
|
||||
if ((item = cJSON_GetObjectItem(root, "theme")) != NULL) {
|
||||
nvs_config_set_string(NVS_CONFIG_THEME_NAME, item->valuestring);
|
||||
}
|
||||
if ((item = cJSON_GetObjectItem(root, "accentColors")) != NULL) {
|
||||
char *colors_str = cJSON_Print(item);
|
||||
nvs_config_set_string(NVS_CONFIG_THEME_COLORS, colors_str);
|
||||
free(colors_str);
|
||||
}
|
||||
|
||||
cJSON_Delete(root);
|
||||
httpd_resp_sendstr(req, "{\"status\":\"ok\"}");
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t register_theme_api_endpoints(httpd_handle_t server, void* ctx)
|
||||
{
|
||||
httpd_uri_t theme_get = {
|
||||
.uri = "/api/theme",
|
||||
.method = HTTP_GET,
|
||||
.handler = theme_get_handler,
|
||||
.user_ctx = ctx
|
||||
};
|
||||
|
||||
httpd_uri_t theme_post = {
|
||||
.uri = "/api/theme",
|
||||
.method = HTTP_POST,
|
||||
.handler = theme_post_handler,
|
||||
.user_ctx = ctx
|
||||
};
|
||||
|
||||
httpd_uri_t theme_options = {
|
||||
.uri = "/api/theme",
|
||||
.method = HTTP_OPTIONS,
|
||||
.handler = theme_options_handler,
|
||||
.user_ctx = ctx
|
||||
};
|
||||
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(server, &theme_get));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(server, &theme_post));
|
||||
ESP_ERROR_CHECK(httpd_register_uri_handler(server, &theme_options));
|
||||
|
||||
return ESP_OK;
|
||||
}
|
9
main/http_server/theme_api.h
Normal file
9
main/http_server/theme_api.h
Normal file
@ -0,0 +1,9 @@
|
||||
#ifndef THEME_API_H
|
||||
#define THEME_API_H
|
||||
|
||||
#include "esp_http_server.h"
|
||||
|
||||
// Register theme API endpoints
|
||||
esp_err_t register_theme_api_endpoints(httpd_handle_t server, void* ctx);
|
||||
|
||||
#endif // THEME_API_H
|
@ -29,9 +29,13 @@
|
||||
#define NVS_CONFIG_BEST_DIFF "bestdiff"
|
||||
#define NVS_CONFIG_SELF_TEST "selftest"
|
||||
#define NVS_CONFIG_OVERHEAT_MODE "overheat_mode"
|
||||
|
||||
#define NVS_CONFIG_SWARM "swarmconfig"
|
||||
|
||||
// Theme configuration
|
||||
#define NVS_CONFIG_THEME_SCHEME "themescheme"
|
||||
#define NVS_CONFIG_THEME_NAME "themename"
|
||||
#define NVS_CONFIG_THEME_COLORS "themecolors"
|
||||
|
||||
char * nvs_config_get_string(const char * key, const char * default_value);
|
||||
void nvs_config_set_string(const char * key, const char * default_value);
|
||||
uint16_t nvs_config_get_u16(const char * key, const uint16_t default_value);
|
||||
|
@ -16,4 +16,4 @@ CONFIG_LWIP_MAX_SOCKETS=16
|
||||
CONFIG_SPIRAM=y
|
||||
CONFIG_SPIRAM_MODE_OCT=y
|
||||
CONFIG_SPIRAM_USE_CAPS_ALLOC=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
CONFIG_SPIRAM_SPEED_80M=y
|
||||
|
Loading…
x
Reference in New Issue
Block a user