improve loading state management in theme service

This commit is contained in:
natsoni 2026-01-09 12:11:12 +01:00
parent eb7a81522f
commit df7b4151eb
No known key found for this signature in database
GPG key ID: C65917583181743B
6 changed files with 70 additions and 40 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

@ -13,7 +13,7 @@ import { Subscription } from 'rxjs';
export class ThemeSelectorComponent implements OnInit {
themeForm: UntypedFormGroup;
themes = ['default', 'contrast', 'softsimon', 'bukele'];
themeSubscription: Subscription;
themeStateSubscription: Subscription;
constructor(
private formBuilder: UntypedFormBuilder,
@ -24,11 +24,13 @@ 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 }) => {
console.log('Theme state changed:', 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 +41,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,7 +10,7 @@ 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;
constructor(
@ -23,6 +23,7 @@ export class ThemeService {
theme = 'default';
this.storageService.setValue('theme-preference', 'default');
}
this.themeState$ = new BehaviorSubject({ theme, loading: false });
this.apply(theme);
}
@ -32,33 +33,41 @@ 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';
document.head.appendChild(this.style); // load the css now
}
this.style.onload = () => {
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);
}
}