frontend: implement appearance configuration form with theme customization options
frontend: update theme initialization to align with OpenVidu Meet themes frontend: remove unused color variables and update styles configuration frontend: enhance theme customization with dynamic color picker and theme loading
This commit is contained in:
parent
7978a10948
commit
8de6d127eb
@ -25,7 +25,7 @@
|
||||
"tsConfig": "src/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": ["src/favicon.ico", "src/assets"],
|
||||
"styles": ["src/styles.scss", "src/colors.scss"],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
|
||||
@ -1 +1,127 @@
|
||||
<p>config works!</p>
|
||||
<div class="ov-page-container ov-mb-xxl">
|
||||
<div class="page-header">
|
||||
<div class="title">
|
||||
<mat-icon class="material-symbols-outlined ov-settings-icon">settings</mat-icon>
|
||||
<h1>Configuration</h1>
|
||||
</div>
|
||||
<p class="subtitle">Customize the look and feel of your OpenVidu Meet rooms.</p>
|
||||
</div>
|
||||
|
||||
@if (isLoading()) {
|
||||
<div class="ov-page-loading">
|
||||
<div class="loading-content">
|
||||
<div class="loading-header">
|
||||
<div class="loading-title">
|
||||
<mat-icon class="ov-settings-icon loading-icon">settings</mat-icon>
|
||||
<h1>Loading theme configuration</h1>
|
||||
</div>
|
||||
<p class="loading-subtitle">Please wait while we fetch your theme settings...</p>
|
||||
</div>
|
||||
|
||||
<div class="loading-spinner-container">
|
||||
<mat-spinner diameter="48"></mat-spinner>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="page-content">
|
||||
<!-- Theme Configuration Section -->
|
||||
<mat-card class="section-card theme-config-card">
|
||||
<mat-card-header>
|
||||
<div mat-card-avatar>
|
||||
<mat-icon class="section-icon">palette</mat-icon>
|
||||
</div>
|
||||
<mat-card-title>Appearance</mat-card-title>
|
||||
<mat-card-subtitle>Configure custom appearance for your rooms</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content class="ov-mt-sm">
|
||||
<form [formGroup]="appearanceForm" class="theme-form">
|
||||
<!-- Enable/Disable Toggle -->
|
||||
<h4 class="section-title">Custom Theme</h4>
|
||||
<div class="theme-toggle ov-mt-xs">
|
||||
<span>Enable custom theme</span>
|
||||
<mat-slide-toggle
|
||||
formControlName="enabled"
|
||||
[matTooltip]="!isThemeEnabled ? 'Enable custom theme to override default appearance' : ''"
|
||||
[matTooltipDisabled]="isThemeEnabled"
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
<!-- Theme Configuration Form -->
|
||||
@if (isThemeEnabled) {
|
||||
<!-- Base theme (light / dark) -->
|
||||
<div class="theme-section">
|
||||
<h4 class="section-title">Base Theme</h4>
|
||||
<p class="section-description ov-mt-xs">
|
||||
Select the foundation theme for your custom appearance.
|
||||
</p>
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Base Theme</mat-label>
|
||||
<mat-select formControlName="baseTheme">
|
||||
@for (theme of baseThemeOptions; track theme) {
|
||||
<mat-option [value]="theme">
|
||||
{{ theme.charAt(0).toUpperCase() + theme.slice(1).toLowerCase() }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (appearanceForm.get('baseTheme')?.hasError('required')) {
|
||||
<mat-error>Base theme is required</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Color Pickers -->
|
||||
<div class="theme-section">
|
||||
<h4 class="section-title">Color Customization</h4>
|
||||
<p class="section-description ov-mt-xs">
|
||||
Customize the colors of your theme. Click on any color to modify it.
|
||||
</p>
|
||||
<div class="color-picker-grid ov-mt-md">
|
||||
@for (colorConfig of colorFields; track colorConfig.key) {
|
||||
<div class="color-picker-item" (click)="focusColorInput(colorConfig.key)">
|
||||
<span class="color-label">{{ colorConfig.label }}</span>
|
||||
<div class="color-circle-wrapper">
|
||||
<input
|
||||
[id]="colorConfig.key"
|
||||
type="color"
|
||||
[formControlName]="colorConfig.key"
|
||||
[value]="getColorValue(colorConfig.key)"
|
||||
class="color-input"
|
||||
/>
|
||||
<div
|
||||
class="color-circle"
|
||||
[style.background-color]="getColorValue(colorConfig.key)"
|
||||
[class.has-custom-color]="hasCustomColor(colorConfig.key)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</mat-card-content>
|
||||
|
||||
@if (isThemeEnabled) {
|
||||
<!-- Action Buttons -->
|
||||
<mat-card-actions>
|
||||
<button
|
||||
mat-button
|
||||
class="primary-button"
|
||||
(click)="onSaveAppearanceConfig()"
|
||||
[disabled]="appearanceForm.invalid || !hasChanges()"
|
||||
>
|
||||
<mat-icon>save</mat-icon>
|
||||
Save Theme Configuration
|
||||
</button>
|
||||
<button mat-stroked-button (click)="onResetForm()" [disabled]="!hasChanges()">
|
||||
<mat-icon>undo</mat-icon>
|
||||
Reset Changes
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
}
|
||||
</mat-card>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,204 @@
|
||||
@import '../../../../../../../src/assets/styles/design-tokens';
|
||||
|
||||
.ov-page-container {
|
||||
button {
|
||||
padding: var(--ov-meet-button-padding-vertical) var(--ov-meet-button-padding-horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
// Theme Configuration Section
|
||||
.theme-config-card {
|
||||
.theme-form {
|
||||
@extend .ov-settings-form-section;
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 var(--ov-meet-spacing-md) var(--ov-meet-spacing-md) 0;
|
||||
|
||||
::ng-deep button {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Input field styling
|
||||
.mat-mdc-form-field {
|
||||
margin-bottom: var(--ov-meet-spacing-lg);
|
||||
|
||||
::ng-deep .mat-mdc-text-field-wrapper {
|
||||
background-color: var(--ov-meet-surface-variant);
|
||||
border-radius: var(--ov-meet-border-radius-sm);
|
||||
}
|
||||
|
||||
::ng-deep .mdc-notched-outline__leading,
|
||||
::ng-deep .mdc-notched-outline__notch,
|
||||
::ng-deep .mdc-notched-outline__trailing {
|
||||
border-color: var(--ov-meet-border-color);
|
||||
}
|
||||
}
|
||||
|
||||
// Color picker grid layout - responsive and clean
|
||||
.color-picker-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: var(--ov-meet-spacing-xl);
|
||||
margin: var(--ov-meet-spacing-lg) 0;
|
||||
|
||||
// Tablet and larger - maintain 4 columns
|
||||
@include ov-tablet-up {
|
||||
gap: var(--ov-meet-spacing-xxl);
|
||||
}
|
||||
|
||||
// Small tablets - 2 columns
|
||||
@include ov-tablet-down {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--ov-meet-spacing-lg);
|
||||
}
|
||||
|
||||
// Mobile - single column
|
||||
@include ov-mobile-down {
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--ov-meet-spacing-md);
|
||||
}
|
||||
}
|
||||
|
||||
.color-picker-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
opacity 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
// opacity: 0.9;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.color-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: var(--ov-meet-text-color-primary);
|
||||
text-align: center;
|
||||
margin-bottom: var(--ov-meet-spacing-xs);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.color-circle-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.color-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
z-index: 2;
|
||||
|
||||
// &::-webkit-color-swatch-wrapper {
|
||||
// padding: 0;
|
||||
// border: none;
|
||||
// border-radius: 50%;
|
||||
// }
|
||||
|
||||
// &::-webkit-color-swatch {
|
||||
// border: none;
|
||||
// border-radius: 50%;
|
||||
// }
|
||||
|
||||
// &::-moz-color-swatch {
|
||||
// border: none;
|
||||
// border-radius: 50%;
|
||||
// }
|
||||
}
|
||||
|
||||
.color-circle {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: var(--ov-meet-radius-lg);
|
||||
border: 3px solid var(--ov-meet-border-color);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset colors section
|
||||
.reset-colors-section {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: var(--ov-meet-spacing-lg);
|
||||
padding-top: var(--ov-meet-spacing-lg);
|
||||
border-top: 1px solid var(--ov-meet-border-color);
|
||||
|
||||
.reset-colors-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ov-meet-spacing-xs);
|
||||
font-size: 0.875rem;
|
||||
color: var(--ov-meet-text-color-secondary);
|
||||
border-color: var(--ov-meet-border-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--ov-meet-color-error);
|
||||
border-color: var(--ov-meet-color-error);
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Card Actions - responsive button layout
|
||||
.mat-mdc-card-actions {
|
||||
padding: var(--ov-meet-spacing-lg) var(--ov-meet-spacing-xl);
|
||||
gap: var(--ov-meet-spacing-sm);
|
||||
border-top: 1px solid var(--ov-meet-border-color);
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@include ov-mobile-down {
|
||||
flex-direction: column;
|
||||
|
||||
.mat-mdc-button,
|
||||
.mat-mdc-raised-button,
|
||||
.mat-mdc-stroked-button {
|
||||
width: 100%;
|
||||
margin: var(--ov-meet-spacing-xs) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include ov-tablet-up {
|
||||
justify-content: flex-start;
|
||||
|
||||
button:first-child {
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,246 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { GlobalConfigService, NotificationService } from '@lib/services';
|
||||
import { MeetAppearanceConfig, MeetRoomTheme, MeetRoomThemeMode } from '@lib/typings/ce';
|
||||
import {
|
||||
OPENVIDU_COMPONENTS_DARK_THEME,
|
||||
OPENVIDU_COMPONENTS_LIGHT_THEME,
|
||||
OpenViduThemeService
|
||||
} from 'openvidu-components-angular';
|
||||
|
||||
type ColorField = 'backgroundColor' | 'primaryColor' | 'secondaryColor' | 'surfaceColor';
|
||||
|
||||
interface ThemeColors {
|
||||
backgroundColor: string;
|
||||
primaryColor: string;
|
||||
secondaryColor: string;
|
||||
surfaceColor: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ov-config',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
templateUrl: './config.component.html',
|
||||
styleUrl: './config.component.scss'
|
||||
selector: 'ov-config',
|
||||
standalone: true,
|
||||
imports: [
|
||||
MatCardModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
MatTooltipModule,
|
||||
MatProgressSpinnerModule,
|
||||
MatDividerModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
templateUrl: './config.component.html',
|
||||
styleUrl: './config.component.scss'
|
||||
})
|
||||
export class ConfigComponent {
|
||||
export class ConfigComponent implements OnInit {
|
||||
isLoading = signal(true);
|
||||
hasChanges = signal(false);
|
||||
|
||||
appearanceForm = new FormGroup({
|
||||
enabled: new FormControl<boolean>(false, { nonNullable: true }),
|
||||
baseTheme: new FormControl<MeetRoomThemeMode>(MeetRoomThemeMode.LIGHT, {
|
||||
validators: [Validators.required],
|
||||
nonNullable: true
|
||||
}),
|
||||
backgroundColor: new FormControl<string>('', { nonNullable: true }),
|
||||
primaryColor: new FormControl<string>('', { nonNullable: true }),
|
||||
secondaryColor: new FormControl<string>('', { nonNullable: true }),
|
||||
surfaceColor: new FormControl<string>('', { nonNullable: true })
|
||||
});
|
||||
|
||||
baseThemeOptions: MeetRoomThemeMode[] = [MeetRoomThemeMode.LIGHT, MeetRoomThemeMode.DARK];
|
||||
|
||||
// Color picker configuration
|
||||
colorFields: Array<{ key: ColorField; label: string }> = [
|
||||
{ key: 'backgroundColor', label: 'Background' },
|
||||
{ key: 'primaryColor', label: 'Primary' },
|
||||
{ key: 'secondaryColor', label: 'Secondary' },
|
||||
{ key: 'surfaceColor', label: 'Surface' }
|
||||
];
|
||||
|
||||
private initialFormValue: MeetRoomTheme | null = null;
|
||||
|
||||
// Default color values based on theme
|
||||
private readonly defaultColors: Record<MeetRoomThemeMode, ThemeColors> = {
|
||||
[MeetRoomThemeMode.LIGHT]: {
|
||||
backgroundColor: OPENVIDU_COMPONENTS_LIGHT_THEME['--ov-background-color'] as string,
|
||||
primaryColor: OPENVIDU_COMPONENTS_LIGHT_THEME['--ov-primary-action-color'] as string,
|
||||
secondaryColor: OPENVIDU_COMPONENTS_LIGHT_THEME['--ov-secondary-action-color'] as string,
|
||||
surfaceColor: OPENVIDU_COMPONENTS_LIGHT_THEME['--ov-surface-color'] as string
|
||||
},
|
||||
[MeetRoomThemeMode.DARK]: {
|
||||
backgroundColor: OPENVIDU_COMPONENTS_DARK_THEME['--ov-background-color'] as string,
|
||||
primaryColor: OPENVIDU_COMPONENTS_DARK_THEME['--ov-primary-action-color'] as string,
|
||||
secondaryColor: OPENVIDU_COMPONENTS_DARK_THEME['--ov-secondary-action-color'] as string,
|
||||
surfaceColor: OPENVIDU_COMPONENTS_DARK_THEME['--ov-surface-color'] as string
|
||||
}
|
||||
};
|
||||
|
||||
constructor(
|
||||
private configService: GlobalConfigService,
|
||||
private notificationService: NotificationService
|
||||
) {
|
||||
// Track form changes
|
||||
this.appearanceForm.valueChanges.subscribe(() => {
|
||||
this.checkForChanges();
|
||||
});
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.isLoading.set(true);
|
||||
try {
|
||||
await this.loadAppearanceConfig();
|
||||
} catch (error) {
|
||||
console.error('Error during component initialization:', error);
|
||||
this.notificationService.showSnackbar('Failed to initialize theme configuration');
|
||||
} finally {
|
||||
this.isLoading.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Form state getters
|
||||
get isThemeEnabled(): boolean {
|
||||
return this.appearanceForm.get('enabled')?.value ?? false;
|
||||
}
|
||||
|
||||
// Form actions
|
||||
onResetForm(): void {
|
||||
if (this.initialFormValue) {
|
||||
this.appearanceForm.patchValue(this.initialFormValue);
|
||||
this.hasChanges.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Color management methods
|
||||
getColorValue(colorField: ColorField): string {
|
||||
const formValue = this.appearanceForm.get(colorField)?.value;
|
||||
if (formValue?.trim()) {
|
||||
return formValue;
|
||||
}
|
||||
|
||||
const baseTheme = this.appearanceForm.get('baseTheme')?.value || MeetRoomThemeMode.LIGHT;
|
||||
return this.defaultColors[baseTheme][colorField];
|
||||
}
|
||||
|
||||
focusColorInput(colorField: ColorField): void {
|
||||
const inputElement = document.getElementById(colorField) as HTMLInputElement;
|
||||
inputElement?.click();
|
||||
}
|
||||
|
||||
hasCustomColor(colorField: ColorField): boolean {
|
||||
const formValue = this.appearanceForm.get(colorField)?.value;
|
||||
return Boolean(formValue?.trim());
|
||||
}
|
||||
|
||||
// Configuration management
|
||||
private async loadAppearanceConfig(): Promise<void> {
|
||||
try {
|
||||
const { appearance } = await this.configService.getRoomsAppearanceConfig();
|
||||
const themeConfig = appearance?.themes?.[0];
|
||||
|
||||
if (themeConfig) {
|
||||
this.appearanceForm.patchValue({
|
||||
enabled: themeConfig.enabled,
|
||||
baseTheme: themeConfig.baseTheme,
|
||||
backgroundColor: themeConfig.backgroundColor || '',
|
||||
primaryColor: themeConfig.primaryColor || '',
|
||||
secondaryColor: themeConfig.secondaryColor || '',
|
||||
surfaceColor: themeConfig.surfaceColor || ''
|
||||
});
|
||||
} else {
|
||||
// Set default values
|
||||
this.appearanceForm.patchValue({
|
||||
enabled: false,
|
||||
baseTheme: MeetRoomThemeMode.LIGHT,
|
||||
backgroundColor: '',
|
||||
primaryColor: '',
|
||||
secondaryColor: '',
|
||||
surfaceColor: ''
|
||||
});
|
||||
}
|
||||
|
||||
this.storeInitialValues();
|
||||
} catch (error) {
|
||||
console.error('Error loading appearance config:', error);
|
||||
this.appearanceForm.patchValue({
|
||||
enabled: false,
|
||||
baseTheme: MeetRoomThemeMode.LIGHT,
|
||||
backgroundColor: '',
|
||||
primaryColor: '',
|
||||
secondaryColor: '',
|
||||
surfaceColor: ''
|
||||
});
|
||||
this.storeInitialValues();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private storeInitialValues(): void {
|
||||
this.initialFormValue = { ...this.appearanceForm.value } as MeetRoomTheme;
|
||||
this.hasChanges.set(false);
|
||||
}
|
||||
|
||||
private checkForChanges(): void {
|
||||
if (!this.initialFormValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = this.appearanceForm.value;
|
||||
const hasChangesDetected = JSON.stringify(currentValue) !== JSON.stringify(this.initialFormValue);
|
||||
this.hasChanges.set(hasChangesDetected);
|
||||
if (!currentValue.enabled) {
|
||||
this.onSaveAppearanceConfig();
|
||||
}
|
||||
}
|
||||
|
||||
async onSaveAppearanceConfig(): Promise<void> {
|
||||
if (this.appearanceForm.invalid) {
|
||||
this.notificationService.showSnackbar('Please fix form errors before saving');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = this.appearanceForm.value;
|
||||
|
||||
try {
|
||||
const appearanceConfig: MeetAppearanceConfig = {
|
||||
themes: [this.createThemeFromFormData(formData as MeetRoomTheme)]
|
||||
};
|
||||
|
||||
await this.configService.saveRoomsAppearanceConfig(appearanceConfig);
|
||||
this.notificationService.showSnackbar('Theme configuration saved successfully');
|
||||
this.storeInitialValues();
|
||||
} catch (error) {
|
||||
console.error('Error saving appearance config:', error);
|
||||
this.notificationService.showSnackbar('Failed to save theme configuration');
|
||||
}
|
||||
}
|
||||
|
||||
private createThemeFromFormData(formData: MeetRoomTheme): MeetRoomTheme {
|
||||
const baseTheme = formData.baseTheme ?? MeetRoomThemeMode.LIGHT;
|
||||
const defaults = this.defaultColors[baseTheme];
|
||||
|
||||
return {
|
||||
enabled: formData.enabled,
|
||||
name: 'default',
|
||||
baseTheme,
|
||||
backgroundColor: formData.backgroundColor?.trim() ? formData.backgroundColor : defaults.backgroundColor,
|
||||
primaryColor: formData.primaryColor?.trim() ? formData.primaryColor : defaults.primaryColor,
|
||||
secondaryColor: formData.secondaryColor?.trim() ? formData.secondaryColor : defaults.secondaryColor,
|
||||
surfaceColor: formData.surfaceColor?.trim() ? formData.surfaceColor : defaults.surfaceColor
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
[recordingActivityShowRecordingsList]="false"
|
||||
[activitiesPanelBroadcastingActivity]="false"
|
||||
[showDisconnectionDialog]="false"
|
||||
[showThemeSelector]="!features().showThemeSelector"
|
||||
(onRoomCreated)="onRoomCreated($event)"
|
||||
(onParticipantConnected)="onParticipantConnected($event)"
|
||||
(onParticipantLeft)="onParticipantLeft($event)"
|
||||
|
||||
@ -20,6 +20,7 @@ import {
|
||||
ApplicationFeatures,
|
||||
AuthService,
|
||||
FeatureConfigurationService,
|
||||
GlobalConfigService,
|
||||
MeetingService,
|
||||
NavigationService,
|
||||
NotificationService,
|
||||
@ -49,6 +50,7 @@ import {
|
||||
LeaveButtonDirective,
|
||||
OpenViduComponentsUiModule,
|
||||
OpenViduService,
|
||||
OpenViduThemeService,
|
||||
ParticipantLeftEvent,
|
||||
ParticipantLeftReason,
|
||||
ParticipantModel,
|
||||
@ -128,7 +130,9 @@ export class MeetingComponent implements OnInit {
|
||||
protected navigationService: NavigationService,
|
||||
protected notificationService: NotificationService,
|
||||
protected clipboard: Clipboard,
|
||||
protected viewportService: ViewportService
|
||||
protected viewportService: ViewportService,
|
||||
protected ovThemeService: OpenViduThemeService,
|
||||
protected configService: GlobalConfigService
|
||||
) {
|
||||
this.features = this.featureConfService.features;
|
||||
}
|
||||
@ -281,6 +285,22 @@ export class MeetingComponent implements OnInit {
|
||||
await this.roomService.loadRoomConfig(this.roomId);
|
||||
this.showMeeting = true;
|
||||
|
||||
const { appearance } = await this.configService.getRoomsAppearanceConfig();
|
||||
console.log('Loaded appearance config:', appearance);
|
||||
if (appearance.themes.length > 0 && appearance.themes[0].enabled) {
|
||||
const theme = appearance.themes[0];
|
||||
this.ovThemeService.updateThemeVariables({
|
||||
'--ov-primary-action-color': theme.primaryColor,
|
||||
'--ov-secondary-action-color': theme.secondaryColor,
|
||||
'--ov-background-color': theme.backgroundColor,
|
||||
'--ov-surface-color': theme.surfaceColor
|
||||
});
|
||||
this.features().showThemeSelector = false;
|
||||
} else {
|
||||
this.ovThemeService.resetThemeVariables();
|
||||
this.features().showThemeSelector = true;
|
||||
}
|
||||
|
||||
combineLatest([
|
||||
this.ovComponentsParticipantService.remoteParticipants$,
|
||||
this.ovComponentsParticipantService.localParticipant$
|
||||
|
||||
@ -26,6 +26,7 @@ export interface ApplicationFeatures {
|
||||
showParticipantList: boolean;
|
||||
showSettings: boolean;
|
||||
showFullscreen: boolean;
|
||||
showThemeSelector: boolean;
|
||||
|
||||
// Permissions
|
||||
canModerateRoom: boolean;
|
||||
@ -49,6 +50,7 @@ const DEFAULT_FEATURES: ApplicationFeatures = {
|
||||
showParticipantList: true,
|
||||
showSettings: true,
|
||||
showFullscreen: true,
|
||||
showThemeSelector: true,
|
||||
|
||||
canModerateRoom: false,
|
||||
canRecordRoom: false,
|
||||
|
||||
@ -28,6 +28,10 @@ export class ThemeService {
|
||||
* 3. Light theme as default
|
||||
*/
|
||||
initializeTheme(): void {
|
||||
// Override available themes in OpenVidu Components to match OpenVidu Meet themes.
|
||||
// OpenVidu Meet users do not know nothing about "classic" theme.
|
||||
this.ovComponentsThemeService.getAllThemes = () => [OpenViduThemeMode.Light, OpenViduThemeMode.Dark];
|
||||
|
||||
const savedTheme = this.getSavedTheme();
|
||||
const systemPreference = this.getSystemPreference();
|
||||
const initialTheme = savedTheme || systemPreference || 'light';
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
// OpenVidu Components Color Variables
|
||||
:root {
|
||||
--ov-background-color: #1f2020;
|
||||
--ov-surface-color: #ffffff;
|
||||
|
||||
--ov-primary-action-color: #273235;
|
||||
--ov-secondary-action-color: #f1f1f1;
|
||||
--ov-accent-action-color: #0089ab;
|
||||
|
||||
--ov-error-color: #eb5144;
|
||||
--ov-warn-color: #ffba53;
|
||||
|
||||
--ov-text-primary-color: #ffffff;
|
||||
--ov-text-surface-color: #1d1d1d;
|
||||
|
||||
--ov-toolbar-buttons-radius: 50%;
|
||||
--ov-leave-button-radius: 10px;
|
||||
--ov-video-radius: 5px;
|
||||
--ov-surface-radius: 5px;
|
||||
}
|
||||
@ -2,48 +2,49 @@
|
||||
* Interface representing the config for a room.
|
||||
*/
|
||||
export interface MeetRoomConfig {
|
||||
chat: MeetChatConfig;
|
||||
recording: MeetRecordingConfig;
|
||||
virtualBackground: MeetVirtualBackgroundConfig;
|
||||
// appearance?: MeetAppearanceConfig;
|
||||
chat: MeetChatConfig;
|
||||
recording: MeetRecordingConfig;
|
||||
virtualBackground: MeetVirtualBackgroundConfig;
|
||||
// appearance?: MeetAppearanceConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface representing the config for recordings in a room.
|
||||
*/
|
||||
export interface MeetRecordingConfig {
|
||||
enabled: boolean;
|
||||
allowAccessTo?: MeetRecordingAccess;
|
||||
enabled: boolean;
|
||||
allowAccessTo?: MeetRecordingAccess;
|
||||
}
|
||||
|
||||
export const enum MeetRecordingAccess {
|
||||
ADMIN = 'admin', // Only admins can access the recording
|
||||
ADMIN_MODERATOR = 'admin_moderator', // Admins and moderators can access
|
||||
ADMIN_MODERATOR_SPEAKER = 'admin_moderator_speaker' // Admins, moderators and speakers can access
|
||||
ADMIN = 'admin', // Only admins can access the recording
|
||||
ADMIN_MODERATOR = 'admin_moderator', // Admins and moderators can access
|
||||
ADMIN_MODERATOR_SPEAKER = 'admin_moderator_speaker', // Admins, moderators and speakers can access
|
||||
}
|
||||
|
||||
export interface MeetChatConfig {
|
||||
enabled: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface MeetVirtualBackgroundConfig {
|
||||
enabled: boolean;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export interface MeetAppearanceConfig {
|
||||
themes: MeetRoomTheme[];
|
||||
themes: MeetRoomTheme[];
|
||||
}
|
||||
|
||||
export interface MeetRoomTheme {
|
||||
name: string;
|
||||
baseTheme: MeetRoomThemeMode;
|
||||
backgroundColor?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
surfaceColor?: string;
|
||||
enabled: boolean;
|
||||
name: string;
|
||||
baseTheme: MeetRoomThemeMode;
|
||||
backgroundColor?: string;
|
||||
primaryColor?: string;
|
||||
secondaryColor?: string;
|
||||
surfaceColor?: string;
|
||||
}
|
||||
|
||||
export const enum MeetRoomThemeMode {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark'
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user