feat: Theme engine

big changes
This commit is contained in:
2026-04-02 00:08:38 +02:00
parent 65b9419869
commit bbb6deb0a2
48 changed files with 6150 additions and 235 deletions

View File

@@ -3,8 +3,12 @@ import {
Component,
OnInit,
OnDestroy,
computed,
effect,
inject,
HostListener
HostListener,
signal,
Type
} from '@angular/core';
import {
Router,
@@ -37,6 +41,11 @@ import {
STORAGE_KEY_CURRENT_USER_ID,
STORAGE_KEY_LAST_VISITED_ROUTE
} from './core/constants';
import {
ThemeNodeDirective,
ThemePickerOverlayComponent,
ThemeService
} from './domains/theme';
@Component({
selector: 'app-root',
@@ -48,12 +57,17 @@ import {
FloatingVoiceControlsComponent,
SettingsModalComponent,
DebugConsoleComponent,
ScreenShareSourcePickerComponent
ScreenShareSourcePickerComponent,
ThemeNodeDirective,
ThemePickerOverlayComponent
],
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App implements OnInit, OnDestroy {
private static readonly THEME_STUDIO_CONTROLS_MARGIN = 16;
private static readonly TITLE_BAR_HEIGHT = 40;
store = inject(Store);
currentRoom = this.store.selectSignal(selectCurrentRoom);
desktopUpdates = inject(DesktopAppUpdateService);
@@ -65,17 +79,120 @@ export class App implements OnInit, OnDestroy {
private notifications = inject(NotificationsFacade);
private settingsModal = inject(SettingsModalService);
private timeSync = inject(TimeSyncService);
private theme = inject(ThemeService);
private voiceSession = inject(VoiceSessionFacade);
private externalLinks = inject(ExternalLinkService);
private electronBridge = inject(ElectronBridgeService);
private deepLinkCleanup: (() => void) | null = null;
private themeStudioControlsDragOffset: { x: number; y: number } | null = null;
private themeStudioControlsBounds: { width: number; height: number } | null = null;
readonly themeStudioFullscreenComponent = signal<Type<unknown> | null>(null);
readonly themeStudioControlsPosition = signal<{ x: number; y: number } | null>(null);
readonly isDraggingThemeStudioControls = signal(false);
readonly appShellLayoutStyles = computed(() => this.theme.getLayoutContainerStyles('appShell'));
readonly serversRailLayoutStyles = computed(() => this.theme.getLayoutItemStyles('serversRail'));
readonly appWorkspaceLayoutStyles = computed(() => this.theme.getLayoutItemStyles('appWorkspace'));
readonly isThemeStudioFullscreen = computed(() => {
return this.settingsModal.isOpen()
&& this.settingsModal.activePage() === 'theme'
&& this.settingsModal.themeStudioFullscreen();
});
readonly isThemeStudioMinimized = computed(() => {
return this.settingsModal.activePage() === 'theme'
&& this.settingsModal.themeStudioMinimized();
});
readonly appWorkspaceShellStyles = computed(() => {
const workspaceStyles = this.appWorkspaceLayoutStyles();
if (!this.isThemeStudioFullscreen()) {
return workspaceStyles;
}
return {
...workspaceStyles,
gridColumn: '1 / -1',
gridRow: '1 / -1'
};
});
readonly themeStudioControlsPositionStyles = computed(() => {
const position = this.themeStudioControlsPosition();
if (!position) {
return {
right: `${App.THEME_STUDIO_CONTROLS_MARGIN}px`,
bottom: `${App.THEME_STUDIO_CONTROLS_MARGIN}px`
};
}
return {
left: `${position.x}px`,
top: `${position.y}px`
};
});
constructor() {
effect(() => {
if (!this.isThemeStudioFullscreen() || this.themeStudioFullscreenComponent()) {
return;
}
void import('./domains/theme/feature/settings/theme-settings.component')
.then((module) => {
this.themeStudioFullscreenComponent.set(module.ThemeSettingsComponent);
});
});
effect(() => {
if (this.isThemeStudioFullscreen()) {
return;
}
this.isDraggingThemeStudioControls.set(false);
this.themeStudioControlsDragOffset = null;
this.themeStudioControlsBounds = null;
});
}
@HostListener('document:click', ['$event'])
onGlobalLinkClick(evt: MouseEvent): void {
this.externalLinks.handleClick(evt);
}
@HostListener('document:keydown', ['$event'])
onGlobalKeyDown(evt: KeyboardEvent): void {
this.theme.handleGlobalShortcut(evt);
}
@HostListener('document:pointermove', ['$event'])
onDocumentPointerMove(event: PointerEvent): void {
if (!this.isDraggingThemeStudioControls() || !this.themeStudioControlsDragOffset || !this.themeStudioControlsBounds) {
return;
}
this.themeStudioControlsPosition.set(this.clampThemeStudioControlsPosition(
event.clientX - this.themeStudioControlsDragOffset.x,
event.clientY - this.themeStudioControlsDragOffset.y,
this.themeStudioControlsBounds.width,
this.themeStudioControlsBounds.height
));
}
@HostListener('document:pointerup')
@HostListener('document:pointercancel')
onDocumentPointerEnd(): void {
if (!this.isDraggingThemeStudioControls()) {
return;
}
this.isDraggingThemeStudioControls.set(false);
this.themeStudioControlsDragOffset = null;
this.themeStudioControlsBounds = null;
}
async ngOnInit(): Promise<void> {
this.theme.initialize();
void this.desktopUpdates.initialize();
await this.databaseService.initialize();
@@ -143,6 +260,45 @@ export class App implements OnInit, OnDestroy {
this.settingsModal.open('updates');
}
startThemeStudioControlsDrag(event: PointerEvent, controlsElement: HTMLElement): void {
if (event.button !== 0) {
return;
}
const rect = controlsElement.getBoundingClientRect();
this.themeStudioControlsBounds = {
width: rect.width,
height: rect.height
};
this.themeStudioControlsDragOffset = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
this.themeStudioControlsPosition.set({
x: rect.left,
y: rect.top
});
this.isDraggingThemeStudioControls.set(true);
event.preventDefault();
}
reopenThemeStudio(): void {
this.settingsModal.restoreMinimizedThemeStudio();
}
minimizeThemeStudio(): void {
this.settingsModal.minimizeThemeStudio();
}
dismissMinimizedThemeStudio(): void {
this.settingsModal.dismissMinimizedThemeStudio();
}
closeThemeStudio(): void {
this.settingsModal.close();
}
async refreshDesktopUpdateContext(): Promise<void> {
await this.desktopUpdates.refreshServerContext();
}
@@ -151,6 +307,18 @@ export class App implements OnInit, OnDestroy {
await this.desktopUpdates.restartToApplyUpdate();
}
private clampThemeStudioControlsPosition(x: number, y: number, width: number, height: number): { x: number; y: number } {
const minX = App.THEME_STUDIO_CONTROLS_MARGIN;
const minY = App.TITLE_BAR_HEIGHT + App.THEME_STUDIO_CONTROLS_MARGIN;
const maxX = Math.max(minX, window.innerWidth - width - App.THEME_STUDIO_CONTROLS_MARGIN);
const maxY = Math.max(minY, window.innerHeight - height - App.THEME_STUDIO_CONTROLS_MARGIN);
return {
x: Math.min(Math.max(minX, x), maxX),
y: Math.min(Math.max(minY, y), maxY)
};
}
private async setupDesktopDeepLinks(): Promise<void> {
const electronApi = this.electronBridge.getApi();