mirror of https://github.com/OpenVidu/openvidu.git
ov-components: add theme selector component and integrate theme management
- Introduced a new theme selector component to allow users to select themes. - Updated settings panel to include the theme selector. - Created shared styles for device and theme selectors. - Refactored existing audio and video device components to use shared styles. - Enhanced storage service to manage theme preferences. - Updated theme service to support classic theme alongside light and dark themes. - Added translations for theme-related strings in multiple languages.master
parent
776c45be3a
commit
9d17c14bcd
|
@ -82,6 +82,15 @@
|
||||||
</mat-list-item>
|
</mat-list-item>
|
||||||
</mat-list>
|
</mat-list>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="theme-section">
|
||||||
|
<mat-list>
|
||||||
|
<mat-list-item class="theme-selector">
|
||||||
|
<mat-icon matListItemIcon class="material-symbols-outlined">routine</mat-icon>
|
||||||
|
<div matListItemTitle>{{ 'PANEL.SETTINGS.THEME' | translate }}</div>
|
||||||
|
<ov-theme-selector matListItemMeta></ov-theme-selector>
|
||||||
|
</mat-list-item>
|
||||||
|
</mat-list>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
|
<div *ngIf="showCameraButton && selectedOption === settingsOptions.VIDEO" class="video-settings">
|
||||||
<ov-video-devices-select
|
<ov-video-devices-select
|
||||||
|
|
|
@ -222,7 +222,19 @@
|
||||||
color: var(--ov-text-surface-color) !important;
|
color: var(--ov-text-surface-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
::ng-deep .mat-mdc-list-item-meta.mdc-list-item__end {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .lang-selector .expand-more-icon,
|
||||||
|
::ng-deep .lang-selector mat-icon,
|
||||||
|
::ng-deep .theme-selector .expand-more-icon,
|
||||||
|
::ng-deep .theme-selector mat-icon {
|
||||||
|
color: var(--ov-text-surface-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .lang-selector div,
|
||||||
|
::ng-deep .theme-selector div,
|
||||||
.input-label {
|
.input-label {
|
||||||
color: var(--ov-text-surface-color) !important;
|
color: var(--ov-text-surface-color) !important;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
@use '../device-selector-shared' as shared;
|
@use '../selector-shared' as shared;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@use '../selector-shared' as shared;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
|
@ -20,92 +22,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-lang-button {
|
.full-lang-button {
|
||||||
display: flex;
|
@include shared.selector-button('lang-icon', 'lang-name', 'expand-icon');
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: var(--ov-surface-color, #ffffff);
|
|
||||||
border: 2px solid var(--ov-border-color, #e0e0e0);
|
|
||||||
border-radius: 12px;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
color: var(--ov-text-primary-color, #333);
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--ov-primary-action-color, #4285f4);
|
|
||||||
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
color: var(--ov-text-surface-color, #666);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-name {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
display: inline-block !important;
|
|
||||||
color: var(--ov-text-surface-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.expand-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
width: 16px;
|
|
||||||
min-width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
min-height: 16px;
|
|
||||||
color: var(--ov-text-secondary-color, #666);
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
vertical-align: bottom;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-expanded='true'] .expand-icon {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::ng-deep .language-menu.mat-mdc-menu-panel {
|
::ng-deep .language-menu.mat-mdc-menu-panel {
|
||||||
border-radius: 12px;
|
@include shared.selector-menu('language-option', 'lang-option-name');
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
|
||||||
border: 1px solid var(--ov-border-color);
|
|
||||||
overflow: hidden;
|
|
||||||
background: var(--ov-surface-color);
|
|
||||||
|
|
||||||
.language-option {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
font-size: 14px;
|
|
||||||
min-height: 48px;
|
|
||||||
color: var(--ov-text-surface-color);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--ov-hover-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: var(--ov-active-color);
|
|
||||||
|
|
||||||
.check-icon {
|
|
||||||
color: var(--ov-text-surface-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-icon {
|
|
||||||
font-size: 18px;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lang-option-name {
|
|
||||||
flex: 1;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -244,3 +244,101 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Selector button mixin
|
||||||
|
@mixin selector-button($icon-class, $name-class, $expand-class) {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: var(--ov-surface-color, #ffffff);
|
||||||
|
border: 2px solid var(--ov-border-color, #e0e0e0);
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
color: var(--ov-text-primary-color, #333);
|
||||||
|
font-weight: 500;
|
||||||
|
min-width: 150px;
|
||||||
|
|
||||||
|
.#{$name-class} {
|
||||||
|
text-transform: capitalize;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--ov-primary-action-color, #4285f4);
|
||||||
|
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$icon-class} {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
color: var(--ov-text-surface-color, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$name-class} {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
display: inline-block !important;
|
||||||
|
color: var(--ov-text-surface-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$expand-class} {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
min-height: 16px;
|
||||||
|
color: var(--ov-text-secondary-color, #666);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-expanded='true'] .#{$expand-class} {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// selector menu mixin
|
||||||
|
@mixin selector-menu($option-class, $option-name-class) {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.12);
|
||||||
|
border: 1px solid var(--ov-border-color);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--ov-surface-color);
|
||||||
|
|
||||||
|
.#{$option-class} {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
font-size: 14px;
|
||||||
|
min-height: 48px;
|
||||||
|
color: var(--ov-text-surface-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--ov-hover-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--ov-active-color);
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
color: var(--ov-text-surface-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.#{$option-name-class} {
|
||||||
|
text-transform: capitalize;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
@use '../selector-shared' as shared;
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: inline-block;
|
||||||
|
.theme-selector-container {
|
||||||
|
.theme-selector-button {
|
||||||
|
@include shared.selector-button('theme-icon', 'theme-name', 'expand-icon');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep .theme-menu.mat-mdc-menu-panel {
|
||||||
|
@include shared.selector-menu('theme-option', 'theme-option-name');
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { OpenViduThemeService } from '../../../services/theme/theme.service';
|
||||||
|
import { OpenViduThemeMode } from '../../../models/theme.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ov-theme-selector',
|
||||||
|
standalone: false,
|
||||||
|
template: `
|
||||||
|
<div class="theme-selector-container">
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
[matMenuTriggerFor]="themeMenu"
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-label="Select theme"
|
||||||
|
class="theme-selector-button"
|
||||||
|
>
|
||||||
|
<span class="theme-name">
|
||||||
|
{{ currentTheme || 'Select theme' }}
|
||||||
|
<mat-icon class="expand-icon">expand_more</mat-icon>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Theme selection menu -->
|
||||||
|
<mat-menu #themeMenu="matMenu" class="theme-menu">
|
||||||
|
@for (theme of predefinedThemes; track theme) {
|
||||||
|
<button
|
||||||
|
mat-menu-item
|
||||||
|
(click)="setTheme(theme)"
|
||||||
|
[attr.id]="'theme-' + theme"
|
||||||
|
[class.selected]="currentTheme === theme"
|
||||||
|
class="theme-option"
|
||||||
|
>
|
||||||
|
@if (currentTheme === theme) {
|
||||||
|
<mat-icon class="check-icon">check</mat-icon>
|
||||||
|
}
|
||||||
|
<span class="theme-option-name">{{ theme }}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</mat-menu>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
styleUrl: './theme-selector.component.scss'
|
||||||
|
})
|
||||||
|
export class ThemeSelectorComponent {
|
||||||
|
protected predefinedThemes: OpenViduThemeMode[] = Object.values(OpenViduThemeMode);
|
||||||
|
constructor(private themeService: OpenViduThemeService) {}
|
||||||
|
|
||||||
|
get currentTheme() {
|
||||||
|
return this.themeService.getCurrentTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(theme: OpenViduThemeMode) {
|
||||||
|
this.themeService.setTheme(theme);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
@use '../device-selector-shared' as shared;
|
@use '../selector-shared' as shared;
|
||||||
|
|
||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "字幕",
|
"CAPTIONS": "字幕",
|
||||||
"DISABLED_AUDIO": "没有音频设备",
|
"DISABLED_AUDIO": "没有音频设备",
|
||||||
"DISABLED_VIDEO": "没有视频设备",
|
"DISABLED_VIDEO": "没有视频设备",
|
||||||
"CAPTIONS_LANG_TEXT": "选择房间参与者将使用的语言。字幕将以该语言显示。"
|
"CAPTIONS_LANG_TEXT": "选择房间参与者将使用的语言。字幕将以该语言显示。",
|
||||||
|
"THEME": "主题"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "背景效果",
|
"TITLE": "背景效果",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "Untertitel",
|
"CAPTIONS": "Untertitel",
|
||||||
"DISABLED_AUDIO": "Audio deaktiviert",
|
"DISABLED_AUDIO": "Audio deaktiviert",
|
||||||
"DISABLED_VIDEO": "Video deaktiviert",
|
"DISABLED_VIDEO": "Video deaktiviert",
|
||||||
"CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Raum verwenden. Die Untertitel werden in dieser Sprache angezeigt."
|
"CAPTIONS_LANG_TEXT": "Wählen Sie die Sprache, die die Teilnehmer der Raum verwenden. Die Untertitel werden in dieser Sprache angezeigt.",
|
||||||
|
"THEME": "Thema"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "Hintergrund-Effekte",
|
"TITLE": "Hintergrund-Effekte",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "Captions",
|
"CAPTIONS": "Captions",
|
||||||
"DISABLED_AUDIO": "Audio disabled",
|
"DISABLED_AUDIO": "Audio disabled",
|
||||||
"DISABLED_VIDEO": "Video disabled",
|
"DISABLED_VIDEO": "Video disabled",
|
||||||
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the room will use. The captions will appear in that language."
|
"CAPTIONS_LANG_TEXT": "Select the language that the participants of the room will use. The captions will appear in that language.",
|
||||||
|
"THEME": "Theme"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "Background effects",
|
"TITLE": "Background effects",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "Subtítulos",
|
"CAPTIONS": "Subtítulos",
|
||||||
"DISABLED_AUDIO": "Audio desactivado",
|
"DISABLED_AUDIO": "Audio desactivado",
|
||||||
"DISABLED_VIDEO": "Video desactivado",
|
"DISABLED_VIDEO": "Video desactivado",
|
||||||
"CAPTIONS_LANG_TEXT": "Selecciona el idioma que usarán los participantes de la sala. Los subtítulos aparecerán en ese idioma."
|
"CAPTIONS_LANG_TEXT": "Selecciona el idioma que usarán los participantes de la sala. Los subtítulos aparecerán en ese idioma.",
|
||||||
|
"THEME": "Tema"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "Efectos de fondo",
|
"TITLE": "Efectos de fondo",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "Les sous-titres",
|
"CAPTIONS": "Les sous-titres",
|
||||||
"DISABLED_AUDIO": "Désactiver l'audio",
|
"DISABLED_AUDIO": "Désactiver l'audio",
|
||||||
"DISABLED_VIDEO": "Désactiver la vidéo",
|
"DISABLED_VIDEO": "Désactiver la vidéo",
|
||||||
"CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la salle utiliseront. Les sous-titres apparaîtront dans cette langue."
|
"CAPTIONS_LANG_TEXT": "Sélectionnez la langue que les participants de la salle utiliseront. Les sous-titres apparaîtront dans cette langue.",
|
||||||
|
"THEME": "Thème"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "Effets de fond",
|
"TITLE": "Effets de fond",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "उपशीर्षक",
|
"CAPTIONS": "उपशीर्षक",
|
||||||
"DISABLED_AUDIO": "ऑडियो अक्षम",
|
"DISABLED_AUDIO": "ऑडियो अक्षम",
|
||||||
"DISABLED_VIDEO": "वीडियो अक्षम",
|
"DISABLED_VIDEO": "वीडियो अक्षम",
|
||||||
"CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग कमरा के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।"
|
"CAPTIONS_LANG_TEXT": "उस भाषा का चयन करें जिसका उपयोग कमरा के प्रतिभागी करेंगे। उपशीर्षक उस भाषा में दिखाई देंगे।",
|
||||||
|
"THEME": "थीम"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "पृष्ठभूमि प्रभाव",
|
"TITLE": "पृष्ठभूमि प्रभाव",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "Sottotitoli",
|
"CAPTIONS": "Sottotitoli",
|
||||||
"DISABLED_AUDIO": "Disattiva l'audio",
|
"DISABLED_AUDIO": "Disattiva l'audio",
|
||||||
"DISABLED_VIDEO": "Disattiva il video",
|
"DISABLED_VIDEO": "Disattiva il video",
|
||||||
"CAPTIONS_LANG_TEXT": "Seleziona la lingua che i partecipanti della stanza useranno. I sottotitoli appariranno in quella lingua."
|
"CAPTIONS_LANG_TEXT": "Seleziona la lingua che i partecipanti della stanza useranno. I sottotitoli appariranno in quella lingua.",
|
||||||
|
"THEME": "Tema"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "Effetti di sfondo",
|
"TITLE": "Effetti di sfondo",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "字幕",
|
"CAPTIONS": "字幕",
|
||||||
"DISABLED_AUDIO": "オーディオを無効にする",
|
"DISABLED_AUDIO": "オーディオを無効にする",
|
||||||
"DISABLED_VIDEO": "ビデオを無効にする",
|
"DISABLED_VIDEO": "ビデオを無効にする",
|
||||||
"CAPTIONS_LANG_TEXT": "ルームの参加者が使用する言語を選択します。キャプションはその言語で表示されます。"
|
"CAPTIONS_LANG_TEXT": "ルームの参加者が使用する言語を選択します。キャプションはその言語で表示されます。",
|
||||||
|
"THEME": "テーマ"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "背景効果",
|
"TITLE": "背景効果",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "Ondertitels",
|
"CAPTIONS": "Ondertitels",
|
||||||
"DISABLED_AUDIO": "Geen audio",
|
"DISABLED_AUDIO": "Geen audio",
|
||||||
"DISABLED_VIDEO": "Geen video",
|
"DISABLED_VIDEO": "Geen video",
|
||||||
"CAPTIONS_LANG_TEXT": "Selecteer de taal die de deelnemers van de kamer zullen gebruiken. De ondertiteling zal in die taal verschijnen."
|
"CAPTIONS_LANG_TEXT": "Selecteer de taal die de deelnemers van de kamer zullen gebruiken. De ondertiteling zal in die taal verschijnen.",
|
||||||
|
"THEME": "Thema"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "Achtergrondeffecten",
|
"TITLE": "Achtergrondeffecten",
|
||||||
|
|
|
@ -106,7 +106,8 @@
|
||||||
"CAPTIONS": "Legendas",
|
"CAPTIONS": "Legendas",
|
||||||
"DISABLED_AUDIO": "Áudio desativado",
|
"DISABLED_AUDIO": "Áudio desativado",
|
||||||
"DISABLED_VIDEO": "Vídeo desativado",
|
"DISABLED_VIDEO": "Vídeo desativado",
|
||||||
"CAPTIONS_LANG_TEXT": "Selecione o idioma que os participantes da sala utilizarão. Os legendas aparecerão nesse idioma."
|
"CAPTIONS_LANG_TEXT": "Selecione o idioma que os participantes da sala utilizarão. Os legendas aparecerão nesse idioma.",
|
||||||
|
"THEME": "Tema"
|
||||||
},
|
},
|
||||||
"BACKGROUND": {
|
"BACKGROUND": {
|
||||||
"TITLE": "Efeitos de fundo",
|
"TITLE": "Efeitos de fundo",
|
||||||
|
|
|
@ -10,6 +10,7 @@ export enum StorageKeys {
|
||||||
LANG = 'lang',
|
LANG = 'lang',
|
||||||
CAPTION_LANG = 'captionLang',
|
CAPTION_LANG = 'captionLang',
|
||||||
BACKGROUND = 'virtualBg',
|
BACKGROUND = 'virtualBg',
|
||||||
|
THEME = 'theme',
|
||||||
TAB_ID = 'tabId',
|
TAB_ID = 'tabId',
|
||||||
ACTIVE_TABS = 'activeTabs'
|
ACTIVE_TABS = 'activeTabs'
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
export enum OpenViduThemeMode {
|
export enum OpenViduThemeMode {
|
||||||
Light = 'light',
|
Light = 'light',
|
||||||
Dark = 'dark',
|
Dark = 'dark',
|
||||||
None = 'none',
|
CLASSIC = 'classic'
|
||||||
Auto = 'auto'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { VideoDevicesComponent } from './components/settings/video-devices/video
|
||||||
import { ApiDirectiveModule } from './directives/api/api.directive.module';
|
import { ApiDirectiveModule } from './directives/api/api.directive.module';
|
||||||
import { OpenViduComponentsDirectiveModule } from './directives/template/openvidu-components-angular.directive.module';
|
import { OpenViduComponentsDirectiveModule } from './directives/template/openvidu-components-angular.directive.module';
|
||||||
import { AppMaterialModule } from './openvidu-components-angular.material.module';
|
import { AppMaterialModule } from './openvidu-components-angular.material.module';
|
||||||
|
import { ThemeSelectorComponent } from './components/settings/theme-selector/theme-selector.component';
|
||||||
|
|
||||||
const publicComponents = [
|
const publicComponents = [
|
||||||
AdminDashboardComponent,
|
AdminDashboardComponent,
|
||||||
|
@ -79,7 +80,8 @@ const privateComponents = [
|
||||||
ParticipantNameInputComponent,
|
ParticipantNameInputComponent,
|
||||||
LangSelectorComponent,
|
LangSelectorComponent,
|
||||||
ToolbarMediaButtonsComponent,
|
ToolbarMediaButtonsComponent,
|
||||||
ToolbarPanelButtonsComponent
|
ToolbarPanelButtonsComponent,
|
||||||
|
ThemeSelectorComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from '../../models/storage.model';
|
} from '../../models/storage.model';
|
||||||
import { LoggerService } from '../logger/logger.service';
|
import { LoggerService } from '../logger/logger.service';
|
||||||
import { CustomDevice } from '../../models/device.model';
|
import { CustomDevice } from '../../models/device.model';
|
||||||
|
import { OpenViduThemeMode } from '../../models/theme.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
|
@ -230,7 +231,7 @@ export class StorageService implements OnDestroy {
|
||||||
if (!this.isStorageAvailable) return;
|
if (!this.isStorageAvailable) return;
|
||||||
|
|
||||||
// Use batch removal for better performance
|
// Use batch removal for better performance
|
||||||
const keysToRemove = TAB_SPECIFIC_KEYS.map(key => `${this.PREFIX_KEY}${tabId}_${key}`);
|
const keysToRemove = TAB_SPECIFIC_KEYS.map((key) => `${this.PREFIX_KEY}${tabId}_${key}`);
|
||||||
|
|
||||||
for (const storageKey of keysToRemove) {
|
for (const storageKey of keysToRemove) {
|
||||||
try {
|
try {
|
||||||
|
@ -339,6 +340,18 @@ export class StorageService implements OnDestroy {
|
||||||
this.remove(StorageKeys.BACKGROUND);
|
this.remove(StorageKeys.BACKGROUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTheme(theme: OpenViduThemeMode): void {
|
||||||
|
this.set(StorageKeys.THEME, theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTheme(): OpenViduThemeMode | null {
|
||||||
|
return this.get(StorageKeys.THEME);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTheme(): void {
|
||||||
|
this.remove(StorageKeys.THEME);
|
||||||
|
}
|
||||||
|
|
||||||
// Core storage methods with improved error handling and caching
|
// Core storage methods with improved error handling and caching
|
||||||
protected set(key: string, item: any): void {
|
protected set(key: string, item: any): void {
|
||||||
if (!this.isStorageAvailable) {
|
if (!this.isStorageAvailable) {
|
||||||
|
@ -403,9 +416,7 @@ export class StorageService implements OnDestroy {
|
||||||
private setLocalValue(key: string, item: any, useCache: boolean = true): void {
|
private setLocalValue(key: string, item: any, useCache: boolean = true): void {
|
||||||
if (!this.isStorageAvailable) return;
|
if (!this.isStorageAvailable) return;
|
||||||
|
|
||||||
const storageKey = this.shouldUseTabSpecificKey(key)
|
const storageKey = this.shouldUseTabSpecificKey(key) ? `${this.PREFIX_KEY}${this.tabId}_${key}` : `${this.PREFIX_KEY}${key}`;
|
||||||
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
|
||||||
: `${this.PREFIX_KEY}${key}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimize serialization for primitive types
|
// Optimize serialization for primitive types
|
||||||
|
@ -436,9 +447,7 @@ export class StorageService implements OnDestroy {
|
||||||
private getLocalValue(key: string, useCache: boolean = true): any {
|
private getLocalValue(key: string, useCache: boolean = true): any {
|
||||||
if (!this.isStorageAvailable) return null;
|
if (!this.isStorageAvailable) return null;
|
||||||
|
|
||||||
const storageKey = this.shouldUseTabSpecificKey(key)
|
const storageKey = this.shouldUseTabSpecificKey(key) ? `${this.PREFIX_KEY}${this.tabId}_${key}` : `${this.PREFIX_KEY}${key}`;
|
||||||
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
|
||||||
: `${this.PREFIX_KEY}${key}`;
|
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
if (useCache && this.cache.has(storageKey)) {
|
if (useCache && this.cache.has(storageKey)) {
|
||||||
|
@ -484,9 +493,7 @@ export class StorageService implements OnDestroy {
|
||||||
private removeLocalValue(key: string): void {
|
private removeLocalValue(key: string): void {
|
||||||
if (!this.isStorageAvailable) return;
|
if (!this.isStorageAvailable) return;
|
||||||
|
|
||||||
const storageKey = this.shouldUseTabSpecificKey(key)
|
const storageKey = this.shouldUseTabSpecificKey(key) ? `${this.PREFIX_KEY}${this.tabId}_${key}` : `${this.PREFIX_KEY}${key}`;
|
||||||
? `${this.PREFIX_KEY}${this.tabId}_${key}`
|
|
||||||
: `${this.PREFIX_KEY}${key}`;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.localStorage.removeItem(storageKey);
|
this.localStorage.removeItem(storageKey);
|
||||||
|
|
|
@ -2,46 +2,25 @@ import { Injectable, Inject } from '@angular/core';
|
||||||
import { DOCUMENT } from '@angular/common';
|
import { DOCUMENT } from '@angular/common';
|
||||||
import { BehaviorSubject, Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { OPENVIDU_DARK_THEME, OPENVIDU_LIGHT_THEME, OpenViduThemeMode, OpenViduThemeVariables } from '../../models/theme.model';
|
import { OPENVIDU_DARK_THEME, OPENVIDU_LIGHT_THEME, OpenViduThemeMode, OpenViduThemeVariables } from '../../models/theme.model';
|
||||||
|
import { StorageService } from '../storage/storage.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for managing OpenVidu component themes dynamically
|
* Service for managing OpenVidu component themes dynamically
|
||||||
*
|
*
|
||||||
* This service allows you to:
|
* This service allows you to:
|
||||||
* - Switch between light, dark, and auto themes
|
* - Switch between light, dark and classic themes
|
||||||
* - Apply custom theme variables
|
* - Apply custom theme variables
|
||||||
* - Listen to theme changes
|
* - Listen to theme changes
|
||||||
* - Integrate with Angular Material themes
|
* - Integrate with Angular Material themes
|
||||||
*
|
*
|
||||||
* @example
|
|
||||||
* ```typescript
|
|
||||||
* // Inject the service
|
|
||||||
* constructor(private themeService: OpenViduThemeService) {}
|
|
||||||
*
|
|
||||||
* // Switch to dark theme
|
|
||||||
* this.themeService.setTheme('dark');
|
|
||||||
*
|
|
||||||
* // Apply custom variables
|
|
||||||
* this.themeService.updateThemeVariables({
|
|
||||||
* '--ov-primary-action-color': '#ff5722',
|
|
||||||
* '--ov-accent-action-color': '#4caf50'
|
|
||||||
* });
|
|
||||||
*
|
|
||||||
* // Listen to theme changes
|
|
||||||
* this.themeService.currentTheme$.subscribe(theme => {
|
|
||||||
* console.log('Current theme:', theme);
|
|
||||||
* });
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class OpenViduThemeService {
|
export class OpenViduThemeService {
|
||||||
private readonly THEME_STORAGE_KEY = 'openvidu-theme';
|
|
||||||
private readonly THEME_ATTRIBUTE = 'data-ov-theme';
|
private readonly THEME_ATTRIBUTE = 'data-ov-theme';
|
||||||
|
private currentThemeSubject = new BehaviorSubject<OpenViduThemeMode>(OpenViduThemeMode.CLASSIC);
|
||||||
private currentThemeSubject = new BehaviorSubject<OpenViduThemeMode>(OpenViduThemeMode.None);
|
|
||||||
private currentVariablesSubject = new BehaviorSubject<OpenViduThemeVariables>({});
|
private currentVariablesSubject = new BehaviorSubject<OpenViduThemeVariables>({});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,9 +33,11 @@ export class OpenViduThemeService {
|
||||||
*/
|
*/
|
||||||
public readonly currentVariables$: Observable<OpenViduThemeVariables> = this.currentVariablesSubject.asObservable();
|
public readonly currentVariables$: Observable<OpenViduThemeVariables> = this.currentVariablesSubject.asObservable();
|
||||||
|
|
||||||
constructor(@Inject(DOCUMENT) private document: Document) {
|
constructor(
|
||||||
|
@Inject(DOCUMENT) private document: Document,
|
||||||
|
protected storageService: StorageService
|
||||||
|
) {
|
||||||
this.initializeTheme();
|
this.initializeTheme();
|
||||||
this.setupSystemThemeListener();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -78,9 +59,9 @@ export class OpenViduThemeService {
|
||||||
* @param theme The theme mode to apply
|
* @param theme The theme mode to apply
|
||||||
*/
|
*/
|
||||||
setTheme(theme: OpenViduThemeMode): void {
|
setTheme(theme: OpenViduThemeMode): void {
|
||||||
this.currentThemeSubject.next(theme);
|
|
||||||
this.applyTheme(theme);
|
this.applyTheme(theme);
|
||||||
this.saveThemeToStorage(theme);
|
this.currentThemeSubject.next(theme);
|
||||||
|
this.storageService.setTheme(theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -152,17 +133,19 @@ export class OpenViduThemeService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private initializeTheme(): void {
|
private initializeTheme(): void {
|
||||||
const savedTheme = this.getThemeFromStorage();
|
const savedTheme = this.storageService.getTheme();
|
||||||
const initialTheme = savedTheme || OpenViduThemeMode.None;
|
const initialTheme = savedTheme || OpenViduThemeMode.CLASSIC;
|
||||||
this.applyTheme(initialTheme);
|
this.applyTheme(initialTheme);
|
||||||
this.currentThemeSubject.next(initialTheme);
|
this.currentThemeSubject.next(initialTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
private applyTheme(theme: OpenViduThemeMode): void {
|
private applyTheme(theme: OpenViduThemeMode): void {
|
||||||
const documentElement = this.document.documentElement;
|
const documentElement = this.document.documentElement;
|
||||||
|
const currentTheme = this.getCurrentTheme();
|
||||||
if (theme === OpenViduThemeMode.Auto || theme === OpenViduThemeMode.None) {
|
if (theme === OpenViduThemeMode.CLASSIC) {
|
||||||
documentElement.removeAttribute(this.THEME_ATTRIBUTE);
|
documentElement.removeAttribute(this.THEME_ATTRIBUTE);
|
||||||
|
const currentVariables = this.getDefaultVariablesForTheme(currentTheme);
|
||||||
|
this.removeCSSVariables(currentVariables);
|
||||||
} else {
|
} else {
|
||||||
documentElement.setAttribute(this.THEME_ATTRIBUTE, theme);
|
documentElement.setAttribute(this.THEME_ATTRIBUTE, theme);
|
||||||
}
|
}
|
||||||
|
@ -182,57 +165,22 @@ export class OpenViduThemeService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private removeCSSVariables(variables: OpenViduThemeVariables): void {
|
||||||
|
const documentElement = this.document.documentElement;
|
||||||
|
|
||||||
|
Object.keys(variables).forEach((property) => {
|
||||||
|
documentElement.style.removeProperty(property);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private getDefaultVariablesForTheme(theme: OpenViduThemeMode): OpenViduThemeVariables {
|
private getDefaultVariablesForTheme(theme: OpenViduThemeMode): OpenViduThemeVariables {
|
||||||
switch (theme) {
|
switch (theme) {
|
||||||
case OpenViduThemeMode.Light:
|
case OpenViduThemeMode.Light:
|
||||||
return OPENVIDU_LIGHT_THEME;
|
return OPENVIDU_LIGHT_THEME;
|
||||||
case OpenViduThemeMode.Dark:
|
case OpenViduThemeMode.Dark:
|
||||||
return OPENVIDU_DARK_THEME;
|
return OPENVIDU_DARK_THEME;
|
||||||
case OpenViduThemeMode.None:
|
|
||||||
return {};
|
|
||||||
case OpenViduThemeMode.Auto:
|
|
||||||
// Auto theme - use system preference
|
|
||||||
return this.prefersDarkMode() ? OPENVIDU_DARK_THEME : OPENVIDU_LIGHT_THEME;
|
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupSystemThemeListener(): void {
|
|
||||||
if (window.matchMedia) {
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
||||||
|
|
||||||
const handleSystemThemeChange = (event: MediaQueryListEvent) => {
|
|
||||||
if (this.getCurrentTheme() === OpenViduThemeMode.Auto) {
|
|
||||||
const defaultVariables = this.getDefaultVariablesForTheme(OpenViduThemeMode.Auto);
|
|
||||||
this.applyCSSVariables(defaultVariables);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the newer addEventListener if available, otherwise use the deprecated addListener
|
|
||||||
if (mediaQuery.addEventListener) {
|
|
||||||
mediaQuery.addEventListener('change', handleSystemThemeChange);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private saveThemeToStorage(theme: OpenViduThemeMode): void {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(this.THEME_STORAGE_KEY, theme);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to save theme to localStorage:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private getThemeFromStorage(): OpenViduThemeMode | null {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(this.THEME_STORAGE_KEY) as OpenViduThemeMode;
|
|
||||||
if (saved && [OpenViduThemeMode.Light, OpenViduThemeMode.Dark, OpenViduThemeMode.Auto].includes(saved)) {
|
|
||||||
return saved;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Failed to read theme from localStorage:', error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue