Merge pull request #6201 from mempool/natsoni/fix-theme-switch

Improve color theme loading and fix Safari issues
This commit is contained in:
mononaut 2026-01-12 15:26:13 +09:00 committed by GitHub
commit 6bbc8dae29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 79 additions and 42 deletions

View file

@ -64,7 +64,8 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
@ViewChild('blockCanvas')
canvas: ElementRef<HTMLCanvasElement>;
themeChangedSubscription: Subscription;
themeStateSubscription: Subscription;
loadedTheme = 'default';
gl: WebGLRenderingContext;
animationFrameRequest: number;
@ -129,7 +130,11 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
if (this.gl) {
this.initCanvas();
this.resizeCanvas();
this.themeChangedSubscription = this.themeService.themeChanged$.subscribe(() => {
this.themeStateSubscription = this.themeService.themeState$.subscribe((state) => {
if (state.loading) {
return;
}
this.loadedTheme = state.theme;
this.scene.setColorFunction(this.getColorFunction());
});
}
@ -184,7 +189,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
}
this.vertexArray.destroy();
this.vertexArray = null;
this.themeChangedSubscription?.unsubscribe();
this.themeStateSubscription?.unsubscribe();
this.searchSubscription?.unsubscribe();
}
@ -670,13 +675,13 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On
break;
}
if (matches) {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
if (this.loadedTheme !== 'contrast' && this.loadedTheme !== 'bukele') {
return (gradient === 'age') ? ageColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000)) : defaultColorFunction(tx, defaultColors.fee, defaultAuditColors, this.relativeTime || (Date.now() / 1000));
} else {
return (gradient === 'age') ? ageColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000)) : contrastColorFunction(tx, contrastColors.fee, contrastAuditColors, this.relativeTime || (Date.now() / 1000));
}
} else {
if (this.themeService.theme !== 'contrast' && this.themeService.theme !== 'bukele') {
if (this.loadedTheme !== 'contrast' && this.loadedTheme !== 'bukele') {
return (gradient === 'age') ? { r: 1, g: 1, b: 1, a: 0.05 } : defaultColorFunction(
tx,
defaultColors.unmatchedfee,

View file

@ -16,7 +16,7 @@ import { ThemeService } from '@app/services/theme.service';
export class FeesBoxComponent implements OnInit, OnDestroy {
isLoading$: Observable<boolean>;
recommendedFees$: Observable<Recommendedfees>;
themeSubscription: Subscription;
themeStateSubscription: Subscription;
gradient = 'linear-gradient(to right, var(--skeleton-bg), var(--skeleton-bg))';
noPriority = 'var(--skeleton-bg)';
fees: Recommendedfees;
@ -42,8 +42,10 @@ export class FeesBoxComponent implements OnInit, OnDestroy {
}
)
);
this.themeSubscription = this.themeService.themeChanged$.subscribe(() => {
this.setFeeGradient();
this.themeStateSubscription = this.themeService.themeState$.subscribe((state) => {
if (!state.loading) {
this.setFeeGradient();
}
});
}
@ -66,6 +68,6 @@ export class FeesBoxComponent implements OnInit, OnDestroy {
}
ngOnDestroy(): void {
this.themeSubscription.unsubscribe();
this.themeStateSubscription.unsubscribe();
}
}

View file

@ -50,6 +50,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
blockDisplayMode: 'size' | 'fees';
blockTransformation = {};
blocksSubscription: Subscription;
themeStateSubscription: Subscription;
mempoolBlocksFull: MempoolBlock[] = [];
mempoolBlockStyles = [];
@ -146,6 +147,12 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.reduceEmptyBlocksToFitScreen(this.mempoolEmptyBlocks);
this.isTabHiddenSubscription = this.stateService.isTabHidden$.subscribe((tabHidden) => this.tabHidden = tabHidden);
this.themeStateSubscription = this.themeService.themeState$.subscribe((state) => {
if (!state.loading) {
this.updateMempoolBlockStyles();
this.cd.markForCheck();
}
});
this.loadingBlocks$ = combineLatest([
this.stateService.isLoadingWebSocket$,
this.stateService.isLoadingMempool$
@ -300,6 +307,7 @@ export class MempoolBlocksComponent implements OnInit, OnChanges, OnDestroy {
this.networkSubscription.unsubscribe();
this.blockDisplayModeSubscription.unsubscribe();
this.timeLtrSubscription.unsubscribe();
this.themeStateSubscription.unsubscribe();
this.chainTipSubscription.unsubscribe();
this.keySubscription.unsubscribe();
this.isTabHiddenSubscription.unsubscribe();

View file

@ -1,3 +1,7 @@
.custom-select {
width: 100px;
}
.custom-select:disabled {
opacity: 0.6;
}

View file

@ -1,4 +1,4 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ChangeDetectionStrategy, Component, OnInit, OnDestroy } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms';
import { ThemeService } from '@app/services/theme.service';
import { Subscription } from 'rxjs';
@ -10,10 +10,10 @@ import { Subscription } from 'rxjs';
standalone: false,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ThemeSelectorComponent implements OnInit {
export class ThemeSelectorComponent implements OnInit, OnDestroy {
themeForm: UntypedFormGroup;
themes = ['default', 'contrast', 'softsimon', 'bukele'];
themeSubscription: Subscription;
themeStateSubscription: Subscription;
constructor(
private formBuilder: UntypedFormBuilder,
@ -24,11 +24,12 @@ export class ThemeSelectorComponent implements OnInit {
this.themeForm = this.formBuilder.group({
theme: ['default']
});
this.themeForm.get('theme')?.setValue(this.themeService.theme);
// Subscribe to theme changes because two instances of this component exist
this.themeSubscription = this.themeService.themeChanged$.subscribe(() => {
if (this.themeForm.get('theme')?.value !== this.themeService.theme){
this.themeForm.get('theme')?.setValue(this.themeService.theme);
this.themeStateSubscription = this.themeService.themeState$.subscribe(({ theme, loading }) => {
this.themeForm.get('theme')?.setValue(theme, { emitEvent: false });
if (loading) {
this.themeForm.get('theme')?.disable({ emitEvent: false });
} else {
this.themeForm.get('theme')?.enable({ emitEvent: false });
}
});
}
@ -39,6 +40,6 @@ export class ThemeSelectorComponent implements OnInit {
}
ngOnDestroy() {
this.themeSubscription.unsubscribe();
this.themeStateSubscription.unsubscribe();
}
}

View file

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import { defaultMempoolFeeColors, contrastMempoolFeeColors } from '@app/app.constants';
import { StorageService } from '@app/services/storage.service';
import { StateService } from '@app/services/state.service';
@ -10,8 +10,9 @@ import { StateService } from '@app/services/state.service';
export class ThemeService {
style: HTMLLinkElement | null = null;
theme: string = 'default';
themeChanged$: Subject<string> = new Subject();
themeState$: BehaviorSubject<{ theme: string; loading: boolean; }>;
mempoolFeeColors: string[] = defaultMempoolFeeColors;
initialLoad: boolean = true;
constructor(
private storageService: StorageService,
@ -23,6 +24,7 @@ export class ThemeService {
theme = 'default';
this.storageService.setValue('theme-preference', 'default');
}
this.themeState$ = new BehaviorSubject({ theme, loading: false });
this.apply(theme);
}
@ -32,33 +34,48 @@ export class ThemeService {
}
this.theme = theme;
if (theme !== 'default') {
this.mempoolFeeColors = (theme === 'contrast' || theme === 'bukele') ? contrastMempoolFeeColors : defaultMempoolFeeColors;
try {
if (!this.style) {
this.style = document.createElement('link');
this.style.rel = 'stylesheet';
this.style.href = `${theme}.css`;
this.style.onerror = (): void => { // something went wrong (eg the css resource does not exist, revert to default)
this.apply('default');
};
document.head.appendChild(this.style); // load the css now
} else {
this.style.href = `${theme}.css`;
}
} catch (err) {
console.log('failed to apply theme stylesheet: ', err);
}
} else {
this.mempoolFeeColors = defaultMempoolFeeColors;
if (theme === 'default') {
if (this.style) {
this.style.remove();
this.style = null;
}
if (!this.stateService.env.customize?.theme) {
this.storageService.setValue('theme-preference', theme);
}
this.mempoolFeeColors = defaultMempoolFeeColors;
this.themeState$.next({ theme, loading: false });
return;
}
if (!this.stateService.env.customize?.theme) {
this.storageService.setValue('theme-preference', theme);
// Load theme stylesheet
this.themeState$.next({ theme, loading: true });
try {
if (!this.style) {
this.style = document.createElement('link');
this.style.rel = 'stylesheet';
if (this.initialLoad) {
this.style.media = 'print'; // Prevent white flash and other CSS issues when using custom theme on initial app load in Safari
}
document.head.appendChild(this.style); // load the css now
}
this.style.onload = () => {
if (this.initialLoad) {
this.style.media = 'all';
this.initialLoad = false;
}
this.mempoolFeeColors = theme === 'contrast' || theme === 'bukele' ? contrastMempoolFeeColors : defaultMempoolFeeColors;
this.themeState$.next({ theme, loading: false });
};
this.style.onerror = () => this.apply('default');
this.style.href = `${theme}.css`;
if (!this.stateService.env.customize?.theme) {
this.storageService.setValue('theme-preference', theme);
}
} catch (err) {
console.log('failed to apply theme stylesheet: ', err);
this.apply('default');
}
this.themeChanged$.next(this.theme);
}
}