import {
CdkPortalOutlet,
ComponentPortal,
} from '@angular/cdk/portal';
import { NgIf } from '@angular/common';
import {
Component,
inject,
Injector,
isDevMode,
runInInjectionContext,
Signal,
signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import {
MatMenu,
MatMenuItem,
MatMenuTrigger,
} from '@angular/material/menu';
import { IconDirective } from '@rxap/material-directives/icon';
import {
ThemeService,
} from '@rxap/ngx-theme';
import {
coerceArray,
IsFunction,
ThemeDensity,
} from '@rxap/utilities';
import { from } from 'rxjs';
import { map } from 'rxjs/operators';
import {
RXAP_SETTINGS_MENU_ITEM,
RXAP_SETTINGS_MENU_ITEM_COMPONENT,
} from '../../tokens';
import { SettingsMenuItem } from '../../types';
@Component({
selector: 'rxap-settings-button',
templateUrl: './settings-button.component.html',
styleUrls: ['./settings-button.component.scss'],
imports: [
MatIconButton,
MatIcon,
MatMenu,
MatMenuTrigger,
MatMenuItem,
CdkPortalOutlet,
IconDirective,
NgIf,
]
})
export class SettingsButtonComponent {
public isDevMode = isDevMode();
public readonly theme = inject(ThemeService);
protected readonly injector = inject(Injector);
customItemComponents: Signal<ComponentPortal<unknown>[]> = toSignal(from(Promise.all(
coerceArray(inject(RXAP_SETTINGS_MENU_ITEM_COMPONENT, { optional: true }))
.map(item => IsFunction(item) ? item() : item),
)).pipe(map(items => items.map(item => new ComponentPortal(item, null, this.injector)))), { initialValue: [] });
customItems = signal(coerceArray(inject(RXAP_SETTINGS_MENU_ITEM, { optional: true })));
private savePreviewDensityValue = false;
private currentDensityValue: ThemeDensity | null = null;
private savePreviewTypographyValue = false;
private currentTypographyValue: string | null = null;
public readonly availableTypographies = this.theme.getAvailableTypographies();
private savePreviewThemeValue = false;
public readonly availableThemes = this.theme.getAvailableThemes();
private currentThemeValue: string | null = null;
previewDensity(density: ThemeDensity) {
this.theme.applyDensity(density);
}
restoreDensity() {
this.theme.applyDensity(this.theme.density());
}
setDensity(density: ThemeDensity) {
this.theme.setDensity(density);
}
previewTypography(typography: string) {
this.theme.applyTypography(typography);
}
restoreTypography() {
this.theme.applyTypography(this.theme.typography());
}
setTypography(typography: string) {
this.theme.setTypography(typography);
}
previewTheme(theme: string) {
this.theme.applyTheme(theme);
}
restoreTheme() {
this.theme.applyTheme(this.theme.themeName());
}
setTheme(theme: string) {
this.theme.setTheme(theme);
}
clickItem(item: SettingsMenuItem) {
runInInjectionContext(this.injector, () => item.action());
}
}
<button [matMenuTriggerFor]="menu" mat-icon-button>
<mat-icon svgIcon="cog"></mat-icon>
</button>
<mat-menu #menu="matMenu">
<button (click)="theme.toggleDarkTheme()" mat-menu-item>
@if (theme.darkMode()) {
<mat-icon svgIcon="brightness-2"></mat-icon>
} @else {
<mat-icon svgIcon="brightness-5"></mat-icon>
}
<span i18n>Mode</span>
</button>
<button [matMenuTriggerFor]="themeMenu" mat-menu-item>
<mat-icon svgIcon="compare"></mat-icon>
<span i18n>Theme</span>
</button>
@for (item of customItems(); track item.label) {
<button (click)="clickItem(item)" mat-menu-item>
<mat-icon *ngIf="item.icon as icon" [rxapIcon]="icon"></mat-icon>
<span>{{ item.label }}</span>
</button>
}
@for (item of customItemComponents(); track item) {
<ng-template [cdkPortalOutlet]="item"></ng-template>
}
</mat-menu>
<mat-menu #themeMenu="matMenu" xPosition="before">
<button [matMenuTriggerFor]="themeDensityMenu" mat-menu-item>
<mat-icon svgIcon="move-resize"></mat-icon>
<span i18n>Density</span>
</button>
@if (availableTypographies?.length) {
<button [matMenuTriggerFor]="themeFontMenu" mat-menu-item>
<mat-icon svgIcon="format-font"></mat-icon>
<span i18n>Font</span>
</button>
}
@if (availableThemes?.length) {
<button [matMenuTriggerFor]="themePresetMenu" mat-menu-item>
<mat-icon svgIcon="shape-outline"></mat-icon>
<span i18n>Preset</span>
</button>
}
</mat-menu>
<mat-menu #themeDensityMenu="matMenu" xPosition="before">
<button (click)="setDensity(0)" (mouseenter)="previewDensity(0)" (mouseleave)="restoreDensity()" mat-menu-item>
<mat-icon svgIcon="size-l"></mat-icon>
<span i18n>Normal</span>
</button>
<button (click)="setDensity(-1)" (mouseenter)="previewDensity(-1)" (mouseleave)="restoreDensity()" mat-menu-item>
<mat-icon svgIcon="size-m"></mat-icon>
<span i18n>Dense</span>
</button>
<button (click)="setDensity(-2)" (mouseenter)="previewDensity(-2)" (mouseleave)="restoreDensity()" mat-menu-item>
<mat-icon svgIcon="size-s"></mat-icon>
<span i18n>Very Dense</span>
</button>
<button (click)="setDensity(-3)" (mouseenter)="previewDensity(-3)" (mouseleave)="restoreDensity()" mat-menu-item>
<mat-icon svgIcon="size-xs"></mat-icon>
<span i18n>Extreme Dense</span>
</button>
</mat-menu>
<mat-menu #themeFontMenu="matMenu" xPosition="before">
@for (typographyName of availableTypographies ?? []; track typographyName) {
<button (click)="setTypography(typographyName)"
(mouseenter)="previewTypography(typographyName)"
(mouseleave)="restoreTypography()"
mat-menu-item>
{{ typographyName }}
</button>
}
</mat-menu>
<mat-menu #themePresetMenu="matMenu" xPosition="before">
@for (themeName of availableThemes ?? []; track themeName) {
<button (click)="setTheme(themeName)"
(mouseenter)="previewTheme(themeName)"
(mouseleave)="restoreTheme()"
mat-menu-item>
{{ themeName }}
</button>
}
</mat-menu>