diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index e4e4d90b4..ee5d8f77c 100755 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -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" diff --git a/main/http_server/axe-os/src/app/app.module.ts b/main/http_server/axe-os/src/app/app.module.ts index 78f4701b7..557c03e2e 100644 --- a/main/http_server/axe-os/src/app/app.module.ts +++ b/main/http_server/axe-os/src/app/app.module.ts @@ -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, diff --git a/main/http_server/axe-os/src/app/components/home/home.component.ts b/main/http_server/axe-os/src/app/components/home/home.component.ts index ddb03afb6..b5fa507c0 100644 --- a/main/http_server/axe-os/src/app/components/home/home.component.ts +++ b/main/http_server/axe-os/src/app/components/home/home.component.ts @@ -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; - - public quickLink$: Observable; - public fallbackQuickLink$: Observable; - public expectedHashRate$: Observable; + public info$!: Observable; + public quickLink$!: Observable; + public fallbackQuickLink$!: Observable; + public expectedHashRate$!: Observable; 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}`; } } - diff --git a/main/http_server/axe-os/src/app/components/settings/settings.component.html b/main/http_server/axe-os/src/app/components/settings/settings.component.html index 2b53b2817..98094243f 100644 --- a/main/http_server/axe-os/src/app/components/settings/settings.component.html +++ b/main/http_server/axe-os/src/app/components/settings/settings.component.html @@ -1,21 +1,24 @@

Settings

- +
- +
+
+ +
-
Current Version: {{(info$ | async)?.version}}
+
Current Version: {{(info$ | async)?.version}}

Latest Release: Check

Clicking this button will connect to GitHub to check for recent updates
-
Current Version: {{(info$ | async)?.version}}
+
Current Version: {{(info$ | async)?.version}}

Latest Release: {{latestRelease.name}}

diff --git a/main/http_server/axe-os/src/app/components/settings/theme-config.component.ts b/main/http_server/axe-os/src/app/components/settings/theme-config.component.ts new file mode 100644 index 000000000..3c56e37ea --- /dev/null +++ b/main/http_server/axe-os/src/app/components/settings/theme-config.component.ts @@ -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: ` +
+
Theme Configuration
+
+
+
Color Scheme
+
+
+ + +
+
+ + +
+
+
+ +
+
Theme Colors
+
+
+ +
{{theme.name}}
+
+
+
+
+
+ ` +}) +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) + ); + } +} diff --git a/main/http_server/axe-os/src/app/layout/service/app.layout.service.ts b/main/http_server/axe-os/src/app/layout/service/app.layout.service.ts index 812b8ad6c..e2ed325ae 100644 --- a/main/http_server/axe-os/src/app/layout/service/app.layout.service.ts +++ b/main/http_server/axe-os/src/app/layout/service/app.layout.service.ts @@ -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(); - private overlayOpen = new Subject(); 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 = 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 = document.getElementById(id); - const cloneLinkElement = themeLink.cloneNode(true); - cloneLinkElement.setAttribute('href', href); - cloneLinkElement.setAttribute('id', id + '-clone'); + // 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); + }); - themeLink.parentNode!.insertBefore( - cloneLinkElement, - themeLink.nextSibling + // 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) ); - cloneLinkElement.addEventListener('load', () => { - themeLink.remove(); - cloneLinkElement.setAttribute('id', id); - }); } changeScale(value: number) { diff --git a/main/http_server/axe-os/src/app/prime-ng.module.ts b/main/http_server/axe-os/src/app/prime-ng.module.ts index d1f9b7998..41cfbb034 100644 --- a/main/http_server/axe-os/src/app/prime-ng.module.ts +++ b/main/http_server/axe-os/src/app/prime-ng.module.ts @@ -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 { } \ No newline at end of file +export class PrimeNGModule { } diff --git a/main/http_server/axe-os/src/app/services/theme.service.ts b/main/http_server/axe-os/src/app/services/theme.service.ts new file mode 100644 index 000000000..031c5ee8b --- /dev/null +++ b/main/http_server/axe-os/src/app/services/theme.service.ts @@ -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 { + return this.http.get('/api/theme'); + } + + // Save theme settings to NVS storage + saveThemeSettings(settings: ThemeSettings): Observable { + return this.http.post('/api/theme', settings); + } +} diff --git a/main/http_server/axe-os/src/styles.scss b/main/http_server/axe-os/src/styles.scss index 09b4661a3..a38012d01 100644 --- a/main/http_server/axe-os/src/styles.scss +++ b/main/http_server/axe-os/src/styles.scss @@ -36,4 +36,128 @@ p-chart>div { &.chart { padding: 10px; } -} \ No newline at end of file +} + +/* 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; + } + } + } + } +} diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c index 767bfb9f3..c6c1c42d7 100644 --- a/main/http_server/http_server.c +++ b/main/http_server/http_server.c @@ -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 = { diff --git a/main/http_server/theme_api.c b/main/http_server/theme_api.c new file mode 100644 index 000000000..53a5629fc --- /dev/null +++ b/main/http_server/theme_api.c @@ -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; +} diff --git a/main/http_server/theme_api.h b/main/http_server/theme_api.h new file mode 100644 index 000000000..64bc46f14 --- /dev/null +++ b/main/http_server/theme_api.h @@ -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 diff --git a/main/nvs_config.h b/main/nvs_config.h index bf8e3ad08..1edd6e40c 100644 --- a/main/nvs_config.h +++ b/main/nvs_config.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); diff --git a/sdkconfig.defaults b/sdkconfig.defaults index c2fb7be60..92838fc1a 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -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 \ No newline at end of file +CONFIG_SPIRAM_SPEED_80M=y