UNPKG

@difizen/mana-core

Version:

171 lines (147 loc) 4.46 kB
import type { Event } from '@difizen/mana-common'; import { isPromiseLike } from '@difizen/mana-common'; import { Emitter, Disposable, objects } from '@difizen/mana-common'; import { prop } from '@difizen/mana-observable'; import { singleton } from '@difizen/mana-syringe'; import { localStorageService } from '../common'; import type { StorageService } from '../common'; import './style/theme-base.less'; export const ThemeServiceSymbol = Symbol('ThemeService'); export type ThemeType = 'light' | 'dark' | 'hc'; export type TokenValue = string | undefined; export interface ExtraTokens { basic?: Record<string, TokenValue>; color?: Record<string, TokenValue>; [key: string]: Record<string, TokenValue> | undefined; } export interface Theme { readonly id: string; readonly type: ThemeType; readonly label: string; readonly description?: string; readonly extraTokens?: ExtraTokens; } export interface ThemeChangeEvent { readonly newTheme: Theme; readonly oldTheme?: Theme | undefined; } export class BuiltinThemeProvider { static readonly darkTheme: Theme = { id: 'dark', type: 'dark', label: 'Dark (mana)', }; static readonly lightTheme: Theme = { id: 'light', type: 'light', label: 'Light (mana)', }; static readonly hcTheme: Theme = { id: 'hc-mana', type: 'hc', label: 'High Contrast (mana)', }; static readonly themes = [ BuiltinThemeProvider.darkTheme, BuiltinThemeProvider.lightTheme, BuiltinThemeProvider.hcTheme, ]; } @singleton() export class ThemeService { protected themes: Record<string, Theme> = {}; @prop() protected activeTheme: Theme | undefined; protected storageService: StorageService = localStorageService; protected readonly themeChange = new Emitter<ThemeChangeEvent>(); readonly onDidColorThemeChange: Event<ThemeChangeEvent> = this.themeChange.event; get themeClassName(): string { return `mana-${this.getCurrentTheme().type}`; } constructor() { if (typeof window !== undefined) { window.addEventListener('mana-theme-change', (event) => { // this.setCurrentTheme(event.); }); } } static get(): ThemeService { const global = window as any; // eslint-disable-line @typescript-eslint/no-explicit-any if (!global[ThemeServiceSymbol]) { const themeService = new ThemeService(); themeService.register(...BuiltinThemeProvider.themes); themeService.startupTheme(); global[ThemeServiceSymbol] = themeService; } return global[ThemeServiceSymbol]; } startupTheme() { this.activeTheme = this.getCurrentTheme(); } register(...themes: Theme[]): Disposable { for (const theme of themes) { this.themes[theme.id] = theme; } this.validateActiveTheme(); return Disposable.create(() => { for (const theme of themes) { delete this.themes[theme.id]; } }); } protected validateActiveTheme(): void { if (!this.activeTheme) { return; } const themeId = this.getCurrentTheme().id; if (themeId !== this.activeTheme.id) { this.setCurrentTheme(themeId); } } getThemes(): Theme[] { const result = []; for (const o in this.themes) { if (Object.prototype.hasOwnProperty.call(this.themes, o)) { result.push(this.themes[o]); } } return result; } getTheme(themeId: string): Theme { return this.themes[themeId] || this.defaultTheme; } setCurrentTheme(themeId: string, tokens: ExtraTokens = {}): void { const newTheme = this.getTheme(themeId); const oldTheme = this.activeTheme; if (oldTheme) { if (oldTheme.id === newTheme.id && !tokens) { return; } } this.activeTheme = objects.mixin(objects.deepClone(newTheme), { extraTokens: { ...tokens }, }); this.storageService.setData('theme', themeId); this.themeChange.fire({ newTheme, oldTheme, }); } getCurrentTheme(): Theme { const maybeThemeId = this.storageService.getData<string>('theme'); let themeId = this.defaultTheme.id; if (!isPromiseLike(maybeThemeId) && maybeThemeId) { themeId = maybeThemeId; } return this.getTheme(themeId); } getActiveTheme(): Theme { return this.activeTheme || this.getCurrentTheme(); } get defaultTheme(): Theme { return BuiltinThemeProvider.lightTheme; } reset(): void { this.setCurrentTheme(this.defaultTheme.id); } }