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:
WantClue 2024-12-28 23:19:33 +01:00 committed by GitHub
parent d9ee113643
commit 188336c1b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 738 additions and 61 deletions

View File

@ -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"

View File

@ -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,

View File

@ -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}`;
}
}

View File

@ -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">

View File

@ -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)
);
}
}

View File

@ -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) {

View File

@ -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 { }

View 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);
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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 = {

View 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;
}

View 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

View File

@ -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);

View File

@ -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