Merge pull request #5811 from tsiegleauq/applause-client

Add applause in client
This commit is contained in:
Sean 2021-02-01 13:34:30 +01:00 committed by GitHub
commit cc65b756c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 1946 additions and 1680 deletions

2431
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -66,6 +66,7 @@
"lz4js": "^0.2.0", "lz4js": "^0.2.0",
"material-icon-font": "git+https://github.com/petergng/materialIconFont.git", "material-icon-font": "git+https://github.com/petergng/materialIconFont.git",
"moment": "^2.27.0", "moment": "^2.27.0",
"ng-particles": "^2.1.11",
"ng2-charts": "^2.4.0", "ng2-charts": "^2.4.0",
"ng2-pdf-viewer": "6.3.2", "ng2-pdf-viewer": "6.3.2",
"ngx-device-detector": "^2.0.0", "ngx-device-detector": "^2.0.0",
@ -78,6 +79,7 @@
"rxjs": "^6.6.2", "rxjs": "^6.6.2",
"tinymce": "5.4.2", "tinymce": "5.4.2",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"tsparticles": "^1.18.11",
"video.js": "^7.8.4", "video.js": "^7.8.4",
"zone.js": "~0.10.2" "zone.js": "~0.10.2"
}, },

View File

@ -1,4 +1,6 @@
import { ApplicationRef, Component } from '@angular/core'; import { ApplicationRef, Component } from '@angular/core';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@ -70,6 +72,8 @@ export class AppComponent {
* @param dataStoreUpgradeService * @param dataStoreUpgradeService
*/ */
public constructor( public constructor(
private matIconRegistry: MatIconRegistry,
private domSanitizer: DomSanitizer,
translate: TranslateService, translate: TranslateService,
appRef: ApplicationRef, appRef: ApplicationRef,
servertimeService: ServertimeService, servertimeService: ServertimeService,
@ -100,6 +104,7 @@ export class AppComponent {
this.overloadArrayFunctions(); this.overloadArrayFunctions();
this.overloadSetFunctions(); this.overloadSetFunctions();
this.overloadModulo(); this.overloadModulo();
this.loadCustomIcons();
// Wait until the App reaches a stable state. // Wait until the App reaches a stable state.
// Required for the Service Worker. // Required for the Service Worker.
@ -204,4 +209,11 @@ export class AppComponent {
enumerable: false enumerable: false
}); });
} }
private loadCustomIcons(): void {
this.matIconRegistry.addSvgIcon(
`clapping_hands`,
this.domSanitizer.bypassSecurityTrustResourceUrl('../assets/svg/clapping_hands.svg')
);
}
} }

View File

@ -0,0 +1,20 @@
import { TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { ApplauseService } from './applause.service';
describe('ApplauseService', () => {
let service: ApplauseService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
});
service = TestBed.inject(ApplauseService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@ -0,0 +1,84 @@
import { Injectable } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { distinctUntilChanged, filter, map } from 'rxjs/operators';
import { ConfigService } from './config.service';
import { HttpService } from '../core-services/http.service';
import { NotifyService } from '../core-services/notify.service';
export interface Applause {
level: number;
presentUsers: number;
}
export enum ApplauseType {
particles = 'applause-type-particles',
bar = 'applause-type-bar'
}
@Injectable({
providedIn: 'root'
})
export class ApplauseService {
private applausePath = '/system/applause';
private applauseNotifyPath = 'applause';
private minApplauseLevel: number;
private maxApplauseLevel: number;
private presentApplauseUsers: number;
public applauseType: ApplauseType;
private applauseLevelSubject: Subject<number> = new Subject<number>();
public applauseLevelObservable = this.applauseLevelSubject.asObservable();
private get maxApplause(): number {
return this.maxApplauseLevel || this.presentApplauseUsers || 0;
}
public constructor(
configService: ConfigService,
private httpService: HttpService,
private notifyService: NotifyService
) {
configService.get<number>('general_system_applause_min_amount').subscribe(minLevel => {
this.minApplauseLevel = minLevel;
});
configService.get<number>('general_system_applause_max_amount').subscribe(maxLevel => {
this.maxApplauseLevel = maxLevel;
});
configService.get<ApplauseType>('general_system_applause_type').subscribe((type: ApplauseType) => {
this.applauseType = type;
});
this.notifyService
.getMessageObservable<Applause>(this.applauseNotifyPath)
.pipe(
map(notify => notify.message as Applause),
/**
* only updates when the effective applause level changes
*/
distinctUntilChanged((prev, curr) => {
return prev.level === curr.level;
}),
filter(curr => {
return curr.level === 0 || curr.level >= this.minApplauseLevel;
})
)
.subscribe(applause => {
this.presentApplauseUsers = applause.presentUsers;
this.applauseLevelSubject.next(applause.level);
});
}
public async sendApplause(): Promise<void> {
await this.httpService.post(this.applausePath);
}
public getApplauseQuote(applauseLevel: number): number {
if (!applauseLevel) {
return 0;
}
const quote = applauseLevel / this.maxApplause || 0;
return quote > 1 ? 1 : quote;
}
}

View File

@ -1,4 +1,4 @@
import { animate, style, transition, trigger } from '@angular/animations'; import { animate, state, style, transition, trigger } from '@angular/animations';
const slideIn = [style({ transform: 'translateX(-85%)' }), animate('600ms ease')]; const slideIn = [style({ transform: 'translateX(-85%)' }), animate('600ms ease')];
const slideOut = [ const slideOut = [
@ -11,4 +11,9 @@ const slideOut = [
) )
]; ];
export const fadeAnimation = trigger('fade', [
state('in', style({ opacity: 1 })),
transition(':enter', [style({ opacity: 0 }), animate(600)]),
transition(':leave', animate(600, style({ opacity: 0 })))
]);
export const navItemAnim = trigger('navItemAnim', [transition(':enter', slideIn), transition(':leave', slideOut)]); export const navItemAnim = trigger('navItemAnim', [transition(':enter', slideIn), transition(':leave', slideOut)]);

View File

@ -0,0 +1,9 @@
<div class="bar-wrapper" *ngIf="isApplauseTypeBar">
<os-progress class="progress-bar" [value]="percent">
<div class="level-indicator">
<div class="level">
<b *ngIf="showLevel && hasLevel" [@fade]="'in'">{{ level }}</b>
</div>
</div>
</os-progress>
</div>

View File

@ -0,0 +1,26 @@
.bar-wrapper {
min-width: 30px;
height: 100%;
.progress-bar {
display: block;
width: 30px;
height: 100%;
margin-left: auto;
}
}
.level-indicator {
height: 50px;
display: block;
text-align: center;
.level {
display: inline-block;
margin-top: 25px;
}
}
.particle-display {
// height: 100%;
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { ApplauseDisplayComponent } from './applause-display.component';
describe('ApplauseDisplayComponent', () => {
let component: ApplauseDisplayComponent;
let fixture: ComponentFixture<ApplauseDisplayComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ApplauseDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,56 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit, ViewEncapsulation } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { Applause, ApplauseService, ApplauseType } from 'app/core/ui-services/applause.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { fadeAnimation } from 'app/shared/animations';
import { BaseViewComponentDirective } from 'app/site/base/base-view';
@Component({
selector: 'os-applause-display',
templateUrl: './applause-display.component.html',
styleUrls: ['./applause-display.component.scss'],
animations: [fadeAnimation],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class ApplauseDisplayComponent extends BaseViewComponentDirective {
public level = 0;
public showLevel: boolean;
public percent = 0;
public get hasLevel(): boolean {
return !!this.level;
}
public get isApplauseTypeBar(): boolean {
return this.applauseService.applauseType === ApplauseType.bar;
}
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
cd: ChangeDetectorRef,
private applauseService: ApplauseService,
configService: ConfigService
) {
super(title, translate, matSnackBar);
this.subscriptions.push(
applauseService.applauseLevelObservable.subscribe(applauseLevel => {
this.level = applauseLevel || 0;
this.percent = this.applauseService.getApplauseQuote(this.level) * 100;
cd.markForCheck();
}),
configService.get<ApplauseType>('general_system_applause_type').subscribe(() => {
cd.markForCheck();
}),
configService.get<boolean>('general_system_applause_show_level').subscribe(show => {
this.showLevel = show;
})
);
}
}

View File

@ -61,9 +61,7 @@
matTooltip="{{ 'Exit live conference and continue livestream' | translate }}" matTooltip="{{ 'Exit live conference and continue livestream' | translate }}"
*ngIf="videoStreamUrl && canSeeLiveStream && !isJitsiDialogOpen" *ngIf="videoStreamUrl && canSeeLiveStream && !isJitsiDialogOpen"
> >
<mat-icon color="warn"> <mat-icon color="warn"> meeting_room </mat-icon>
meeting_room
</mat-icon>
</button> </button>
<!-- mute/unmute button --> <!-- mute/unmute button -->
@ -90,7 +88,11 @@
(click)="enterConversation()" (click)="enterConversation()"
matTooltip="{{ 'Enter live conference' | translate }}" matTooltip="{{ 'Enter live conference' | translate }}"
> >
<mat-icon color="primary" [@fadeInOut]="isEnterMeetingRoomVisible" (@fadeInOut.done)="triggerMeetingRoomButtonAnimation()"> <mat-icon
color="primary"
[@fadeInOut]="isEnterMeetingRoomVisible"
(@fadeInOut.done)="triggerMeetingRoomButtonAnimation()"
>
meeting_room meeting_room
</mat-icon> </mat-icon>
</button> </button>
@ -103,11 +105,23 @@
*ngIf="enableJitsi && !isAccessPermitted" *ngIf="enableJitsi && !isAccessPermitted"
[routerLink]="['/agenda/speakers']" [routerLink]="['/agenda/speakers']"
> >
<mat-icon> <mat-icon> no_meeting_room </mat-icon>
no_meeting_room
</mat-icon>
</a> </a>
</ng-container> </ng-container>
<!-- applause button -->
<button
class="quick-icon indicator"
[disabled]="applauseDisabled"
mat-mini-fab
(click)="sendApplause()"
matTooltip="{{ 'Send applause' | translate }}"
*ngIf="showApplause"
[matBadge]="showApplauseBadge ? applauseLevel : null"
matBadgeColor="accent"
>
<mat-icon svgIcon="clapping_hands"></mat-icon>
</button>
</span> </span>
<span <span
@ -118,6 +132,8 @@
'cast-shadow': showJitsiWindow 'cast-shadow': showJitsiWindow
}" }"
> >
<os-applause-display *ngIf="showApplause && isApplauseTypeBar" class="applause"></os-applause-display>
<!-- open-window button --> <!-- open-window button -->
<button class="toggle-list-button" mat-button (click)="toggleShowJitsi()"> <button class="toggle-list-button" mat-button (click)="toggleShowJitsi()">
<ng-container *ngIf="currentState == state.jitsi"> <ng-container *ngIf="currentState == state.jitsi">
@ -149,7 +165,7 @@
<div <div
class="jitsi-list" class="jitsi-list"
[ngClass]="{ [ngClass]="{
'cdk-visually-hidden': !showJitsiWindow 'hide-height': !showJitsiWindow
}" }"
> >
<ng-container *ngIf="currentState == state.jitsi"> <ng-container *ngIf="currentState == state.jitsi">
@ -175,6 +191,10 @@
<!-- user list --> <!-- user list -->
<div class="room-members" *ngIf="isJitsiActive && isJoined"> <div class="room-members" *ngIf="isJitsiActive && isJoined">
<os-particle-display
*ngIf="isApplauseTypeParticles"
class="room-list-applause-particles"
></os-particle-display>
<div class="member-list"> <div class="member-list">
<ol> <ol>
<li <li
@ -196,6 +216,7 @@
<ng-container *ngIf="currentState == state.stream"> <ng-container *ngIf="currentState == state.stream">
<os-vjs-player <os-vjs-player
[videoUrl]="videoStreamUrl" [videoUrl]="videoStreamUrl"
[showParticles]="isApplauseTypeParticles"
(started)="onSteamStarted()" (started)="onSteamStarted()"
*ngIf="(canSeeLiveStream && !streamActiveInAnotherTab) || streamRunning" *ngIf="(canSeeLiveStream && !streamActiveInAnotherTab) || streamRunning"
></os-vjs-player> ></os-vjs-player>

View File

@ -56,6 +56,7 @@
.jitsi-bar { .jitsi-bar {
display: flex; display: flex;
position: relative;
justify-content: flex-end; justify-content: flex-end;
$wrapper-padding: 5px; $wrapper-padding: 5px;
$bar-height: 40px; $bar-height: 40px;
@ -90,11 +91,20 @@
} }
.list-wrapper { .list-wrapper {
position: relative;
pointer-events: all; pointer-events: all;
min-height: $bar-height; min-height: $bar-height;
padding-top: $wrapper-padding; padding-top: $wrapper-padding;
border-top-right-radius: 4px; border-top-right-radius: 4px;
.applause {
position: absolute;
top: 0;
width: 90px;
left: -90px;
bottom: 50px;
}
.toggle-list-button { .toggle-list-button {
position: relative; position: relative;
line-height: normal; line-height: normal;
@ -121,6 +131,7 @@
.jitsi-list { .jitsi-list {
.content { .content {
height: 40vh; height: 40vh;
max-height: 100%;
clear: both; clear: both;
.disconnected { .disconnected {
@ -136,7 +147,15 @@
} }
.room-members { .room-members {
height: inherit; height: 100%;
position: relative;
.room-list-applause-particles {
position: absolute;
height: 100%;
width: 70px;
right: 0;
}
.member-list { .member-list {
max-height: 100%; max-height: 100%;

View File

@ -17,6 +17,10 @@
.indicator { .indicator {
color: mat-color($primary, default-contrast); color: mat-color($primary, default-contrast);
svg path {
fill: mat-color($primary) !important;
}
} }
.toggle-list-button { .toggle-list-button {

View File

@ -11,6 +11,7 @@ import { ConstantsService } from 'app/core/core-services/constants.service';
import { OperatorService } from 'app/core/core-services/operator.service'; import { OperatorService } from 'app/core/core-services/operator.service';
import { Deferred } from 'app/core/promises/deferred'; import { Deferred } from 'app/core/promises/deferred';
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service'; import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
import { ApplauseService, ApplauseType } from 'app/core/ui-services/applause.service';
import { ConfigService } from 'app/core/ui-services/config.service'; import { ConfigService } from 'app/core/ui-services/config.service';
import { UserMediaPermService } from 'app/core/ui-services/user-media-perm.service'; import { UserMediaPermService } from 'app/core/ui-services/user-media-perm.service';
import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers'; import { UserListIndexType } from 'app/site/agenda/models/view-list-of-speakers';
@ -96,6 +97,10 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
public showJitsiWindow = true; public showJitsiWindow = true;
public muted = true; public muted = true;
public showApplause: boolean;
public applauseDisabled = false;
private applauseTimeout: number;
@ViewChild('jitsi') @ViewChild('jitsi')
private jitsiNode: ElementRef; private jitsiNode: ElementRef;
@ -180,6 +185,26 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
public currentState: ConferenceState; public currentState: ConferenceState;
public isEnterMeetingRoomVisible = true; public isEnterMeetingRoomVisible = true;
public applauseLevel = 0;
private showApplauseLevel: boolean;
private isApplausBarUsed: boolean;
public get showApplauseBadge(): boolean {
return this.showApplauseLevel && this.applauseLevel > 0 && (!this.showJitsiWindow || !this.isApplausBarUsed);
}
private get applauseType(): ApplauseType {
return this.applauseService.applauseType;
}
public get isApplauseTypeBar(): boolean {
return this.applauseType === ApplauseType.bar;
}
public get isApplauseTypeParticles(): boolean {
return this.applauseType === ApplauseType.particles;
}
private configOverwrite = { private configOverwrite = {
startAudioOnly: false, startAudioOnly: false,
// allows jitsi on mobile devices // allows jitsi on mobile devices
@ -237,7 +262,8 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
private constantsService: ConstantsService, private constantsService: ConstantsService,
private configService: ConfigService, private configService: ConfigService,
private closService: CurrentListOfSpeakersService, private closService: CurrentListOfSpeakersService,
private userMediaPermService: UserMediaPermService private userMediaPermService: UserMediaPermService,
private applauseService: ApplauseService
) { ) {
super(titleService, translate, snackBar); super(titleService, translate, snackBar);
} }
@ -350,6 +376,22 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
}), }),
this.configService.get<boolean>('general_system_conference_open_video').subscribe(open => { this.configService.get<boolean>('general_system_conference_open_video').subscribe(open => {
this.configOverwrite.startWithVideoMuted = !open; this.configOverwrite.startWithVideoMuted = !open;
}),
this.configService.get<boolean>('general_system_applause_enable').subscribe(enable => {
this.showApplause = enable;
}),
this.configService.get<number>('general_system_stream_applause_timeout').subscribe(timeout => {
this.applauseTimeout = (timeout || 1) * 1000;
}),
this.configService.get<boolean>('general_system_applause_show_level').subscribe(show => {
this.showApplauseLevel = show;
}),
this.configService.get<any>('general_system_applause_type').subscribe(type => {
if (type === 'applause-type-bar') {
this.isApplausBarUsed = true;
} else {
this.isApplausBarUsed = false;
}
}) })
); );
@ -362,7 +404,10 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
map(los => los?.findUserIndexOnList(this.operator.user.id) ?? -1), map(los => los?.findUserIndexOnList(this.operator.user.id) ?? -1),
distinctUntilChanged() distinctUntilChanged()
) )
.subscribe(userLosIndex => this.autoJoinJitsiByLosIndex(userLosIndex)) .subscribe(userLosIndex => this.autoJoinJitsiByLosIndex(userLosIndex)),
this.applauseService.applauseLevelObservable.subscribe(applauseLevel => {
this.applauseLevel = applauseLevel || 0;
})
); );
} }
@ -595,4 +640,12 @@ export class JitsiComponent extends BaseViewComponentDirective implements OnInit
private setConferenceState(newState: ConferenceState): void { private setConferenceState(newState: ConferenceState): void {
this.currentState = newState; this.currentState = newState;
} }
public sendApplause(): void {
this.applauseDisabled = true;
this.applauseService.sendApplause();
setTimeout(() => {
this.applauseDisabled = false;
}, this.applauseTimeout);
}
} }

View File

@ -0,0 +1,3 @@
<div [osResized]="resizeSubject" class="particle-wrapper">
<Particles id="particles" [options]="particlesOptions" (particlesLoaded)="particlesLoaded($event)"> </Particles>
</div>

View File

@ -0,0 +1,7 @@
.particle-wrapper {
height: 100%;
}
.tsparticles-canvas-el {
pointer-events: none !important;
}

View File

@ -0,0 +1,26 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { E2EImportsModule } from 'e2e-imports.module';
import { ParticleDisplayComponent } from './particle-display.component';
describe('ParticleDisplayComponent', () => {
let component: ParticleDisplayComponent;
let fixture: ComponentFixture<ParticleDisplayComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [E2EImportsModule]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ParticleDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,233 @@
import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { Subject } from 'rxjs';
import { auditTime } from 'rxjs/operators';
import { Container, SizeMode } from 'tsparticles';
import { Shape } from 'tsparticles/dist/Options/Classes/Particles/Shape/Shape';
import { IImageShape } from 'tsparticles/dist/Options/Interfaces/Particles/Shape/IImageShape';
import { Emitters } from 'tsparticles/dist/Plugins/Emitters/Emitters';
import { ApplauseService } from 'app/core/ui-services/applause.service';
import { ConfigService } from 'app/core/ui-services/config.service';
import { ElementSize } from 'app/shared/directives/resized.directive';
import { BaseViewComponentDirective } from 'app/site/base/base-view';
@Component({
selector: 'os-particle-display',
templateUrl: './particle-display.component.html',
styleUrls: ['./particle-display.component.scss'],
encapsulation: ViewEncapsulation.None
})
export class ParticleDisplayComponent extends BaseViewComponentDirective {
public resizeSubject = new Subject<ElementSize>();
private resizeAuditTime = 200;
private particleContainer: Container;
public set particleImage(imageUrl: string) {
this.setParticleImage(imageUrl);
}
public set particleLevel(level: number) {
this.setParticleLevel(level);
}
private noAutomaticParticles = {
value: 0
};
private slowBlinkingOpacity = {
value: 0.8,
animation: {
enable: true,
speed: 1,
sync: false,
minimumValue: 0.3
},
random: {
enable: true,
minimumValue: 0.8
}
};
private imageOptions: IImageShape = {
replace_color: false,
replaceColor: false,
src: '',
width: 24,
height: 24
};
private customImageShape = {
type: 'image',
image: this.imageOptions
};
private charShapeHearth = {
type: 'char',
options: {
char: {
fill: true,
font: 'Verdana',
weight: '200',
style: '',
value: ['❤']
}
}
};
private slightlyRandomSize: any = {
value: 16,
random: {
enable: true,
minimumValue: 10
}
};
private moveUpOptions = {
enable: true,
direction: 'top',
speed: 1.0,
angle: {
offset: 45,
value: 90
},
gravity: {
enable: true,
maxSpeed: 1.5,
acceleration: -3
},
outModes: {
left: 'bounce',
right: 'bounce',
top: 'destroy'
}
};
private slowRandomRotation = {
value: 0,
enable: true,
direction: 'random',
animation: {
enable: true,
speed: 9
},
random: {
enable: true,
minimumValue: 0
}
};
private randomColor = {
value: 'random'
};
private singleBottomEmitter = [
{
direction: 'top',
rate: {
quantity: 0,
delay: 0.33
},
position: {
x: 50,
y: 100
},
size: {
mode: SizeMode.percent,
width: 100
}
}
];
public particlesOptions = {
fpsLimit: 30,
particles: {
number: this.noAutomaticParticles,
opacity: this.slowBlinkingOpacity,
rotate: this.slowRandomRotation,
move: this.moveUpOptions,
color: this.randomColor,
shape: this.charShapeHearth,
size: this.slightlyRandomSize
},
emitters: this.singleBottomEmitter,
detectRetina: true
};
public constructor(
title: Title,
translate: TranslateService,
matSnackBar: MatSnackBar,
configService: ConfigService,
private applauseService: ApplauseService
) {
super(title, translate, matSnackBar);
this.subscriptions.push(
this.resizeSubject.pipe(auditTime(this.resizeAuditTime)).subscribe(size => {
this.updateParticleContainer(size);
}),
applauseService.applauseLevelObservable.subscribe(applause => {
this.particleLevel = this.calcEmitterLevel(applause || 0);
}),
configService.get<string>('general_system_applause_particle_image').subscribe(particleImage => {
this.particleImage = particleImage || undefined;
})
);
}
private setParticleImage(particleImage: string): void {
if (particleImage) {
this.imageOptions.src = particleImage;
(this.particlesOptions.particles.shape as any) = this.customImageShape;
} else {
(this.particlesOptions.particles.shape as any) = this.charShapeHearth;
}
if (this.particleContainer) {
this.particleContainer.options.particles.load(this.particlesOptions.particles as any);
this.refresh();
}
}
private calcEmitterLevel(applauseLevel: number): number {
if (!applauseLevel) {
return 0;
}
let emitterLevel = this.applauseService.getApplauseQuote(applauseLevel);
emitterLevel = Math.ceil(emitterLevel * 10);
return emitterLevel;
}
private setParticleLevel(level: number): void {
if (this.particleContainer) {
const emitters = this.particleContainer.plugins.get('emitters') as Emitters;
if (emitters) {
emitters.array[0].emitterOptions.rate.quantity = level;
}
}
}
private updateParticleContainer(size: ElementSize): void {
if (!size.height || !size.width) {
this.stop();
} else {
this.refresh();
}
}
private stop(): void {
this.particleContainer?.stop();
}
private refresh(): void {
this.particleContainer?.refresh();
}
public particlesLoaded(container: Container): void {
this.particleContainer = container;
this.refresh();
}
}

View File

@ -0,0 +1,10 @@
<div class="progress-wrapper">
<mat-icon class="end-icon" *ngIf="endIcon"> {{ endIcon }} </mat-icon>
<div class="slot">
<ng-content></ng-content>
</div>
<div class="progress-bar">
<div class="buffer bar"></div>
<div class="progress bar" [style.height.%]="value"></div>
</div>
</div>

View File

@ -0,0 +1,36 @@
$bar-margin: 10px;
$bar-border-radius: 15px;
.progress-wrapper {
display: flex;
height: 100%;
flex-direction: column;
.end-icon {
display: block;
margin: auto;
}
.progress-bar {
position: relative;
width: 100%;
.bar {
position: absolute;
right: 0;
left: 0;
bottom: 0;
margin-left: $bar-margin;
margin-right: $bar-margin;
border-radius: $bar-border-radius;
}
.buffer {
height: 100%;
}
.progress {
transition: height 0.5s ease;
}
}
}

View File

@ -0,0 +1,26 @@
@import '~@angular/material/theming';
@mixin os-progress-theme($theme) {
$primary: map-get($theme, primary);
$accent: map-get($theme, accent);
.progress-wrapper {
background-color: mat-color($primary);
}
.end-icon {
color: mat-color($primary, default-contrast);
}
.slot {
color: mat-color($primary, default-contrast);
}
.buffer {
background-color: mat-color($primary, darker);
}
.progress {
background-color: mat-color($accent);
}
}

View File

@ -0,0 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ProgressComponent } from './progress.component';
describe('ProgressComponent', () => {
let component: ProgressComponent;
let fixture: ComponentFixture<ProgressComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ProgressComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ProgressComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,14 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'os-progress',
templateUrl: './progress.component.html',
styleUrls: ['./progress.component.scss']
})
export class ProgressComponent {
@Input()
public value = 0;
@Input()
public endIcon: string;
}

View File

@ -1,4 +1,5 @@
<div class="video-wrapper"> <div class="video-wrapper">
<os-particle-display *ngIf="showParticles" class="applause-particles"></os-particle-display>
<div class="player-container" [ngClass]="{ hide: !isUrlOnline }"> <div class="player-container" [ngClass]="{ hide: !isUrlOnline }">
<video #videoPlayer class="video-js" controls preload="none"></video> <video #videoPlayer class="video-js" controls preload="none"></video>
</div> </div>

View File

@ -1,7 +1,17 @@
.video-wrapper { .video-wrapper {
position: relative;
display: flex; display: flex;
height: 100%;
width: 100%; width: 100%;
min-height: 200px;
.applause-particles {
position: absolute;
display: block;
pointer-events: none !important;
width: 100px;
height: 100%;
z-index: 1;
}
.is-offline-wrapper { .is-offline-wrapper {
width: 100%; width: 100%;

View File

@ -48,6 +48,9 @@ export class VjsPlayerComponent implements OnInit, OnDestroy {
this.checkVideoUrl(); this.checkVideoUrl();
} }
@Input()
public showParticles: boolean;
@Output() @Output()
private started: EventEmitter<void> = new EventEmitter(); private started: EventEmitter<void> = new EventEmitter();

View File

@ -1,10 +1,15 @@
import { Directive, ElementRef, Input, OnInit } from '@angular/core'; import { Directive, ElementRef, Input, OnInit } from '@angular/core';
import { ResizeSensor } from 'css-element-queries'; import { ResizeSensor } from 'css-element-queries';
import { Interface } from 'readline';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
export interface ElementSize {
width: number;
height: number;
}
/** /**
* This directive takes a Subject<void> as input and everytime the surrounding element * This directive takes a Subject<ElementSize> as input and everytime the surrounding element
* was resized, the subject is fired. * was resized, the subject is fired.
* *
* Usage: * Usage:
@ -15,7 +20,7 @@ import { Subject } from 'rxjs';
}) })
export class ResizedDirective implements OnInit { export class ResizedDirective implements OnInit {
@Input() @Input()
public osResized: Subject<void>; public osResized: Subject<ElementSize>;
/** /**
* Old width, to check, if the width has actually changed. * Old width, to check, if the width has actually changed.
@ -54,7 +59,10 @@ export class ResizedDirective implements OnInit {
this.oldHeight = newHeight; this.oldHeight = newHeight;
if (this.osResized) { if (this.osResized) {
this.osResized.next(); this.osResized.next({
width: newWidth,
height: newHeight
});
} }
} }
} }

View File

@ -130,6 +130,10 @@ import { JitsiComponent } from './components/jitsi/jitsi.component';
import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component'; import { VjsPlayerComponent } from './components/vjs-player/vjs-player.component';
import { LiveStreamComponent } from './components/live-stream/live-stream.component'; import { LiveStreamComponent } from './components/live-stream/live-stream.component';
import { ListOfSpeakersContentComponent } from './components/list-of-speakers-content/list-of-speakers-content.component'; import { ListOfSpeakersContentComponent } from './components/list-of-speakers-content/list-of-speakers-content.component';
import { ApplauseDisplayComponent } from './components/applause-display/applause-display.component';
import { ProgressComponent } from './components/progress/progress.component';
import { NgParticlesModule } from 'ng-particles';
import { ParticleDisplayComponent } from './components/particle-display/particle-display.component';
/** /**
* Share Module for all "dumb" components and pipes. * Share Module for all "dumb" components and pipes.
@ -194,7 +198,8 @@ import { ListOfSpeakersContentComponent } from './components/list-of-speakers-co
PblNgridTargetEventsModule, PblNgridTargetEventsModule,
PdfViewerModule, PdfViewerModule,
NgxMaterialTimepickerModule, NgxMaterialTimepickerModule,
ChartsModule ChartsModule,
NgParticlesModule
], ],
exports: [ exports: [
FormsModule, FormsModule,
@ -364,7 +369,10 @@ import { ListOfSpeakersContentComponent } from './components/list-of-speakers-co
JitsiComponent, JitsiComponent,
VjsPlayerComponent, VjsPlayerComponent,
LiveStreamComponent, LiveStreamComponent,
ListOfSpeakersContentComponent ListOfSpeakersContentComponent,
ApplauseDisplayComponent,
ProgressComponent,
ParticleDisplayComponent
], ],
providers: [ providers: [
{ {

View File

@ -13,7 +13,6 @@
</os-head-bar> </os-head-bar>
<mat-card class="spacer-bottom-60" [ngClass]="isEditing ? 'os-form-card' : 'os-card'"> <mat-card class="spacer-bottom-60" [ngClass]="isEditing ? 'os-form-card' : 'os-card'">
<mat-card [ngClass]="isEditing ? 'os-form-card' : 'os-card'">
<ng-container *ngIf="!isEditing"> <ng-container *ngIf="!isEditing">
<div class="app-content"> <div class="app-content">
<h1>{{ startContent.general_event_welcome_title | translate }}</h1> <h1>{{ startContent.general_event_welcome_title | translate }}</h1>

View File

@ -9,43 +9,44 @@ $narrow-spacing: (
); );
/** Import brand theme */ /** Import brand theme */
@import './assets/styles/themes/default-dark.scss'; @import '~assets/styles/themes/default-dark.scss';
@import './assets/styles/themes/default-light.scss'; @import '~assets/styles/themes/default-light.scss';
@import './assets/styles/themes/green-dark.scss'; @import '~assets/styles/themes/green-dark.scss';
@import './assets/styles/themes/green-light.scss'; @import '~assets/styles/themes/green-light.scss';
@import './assets/styles/themes/red-dark.scss'; @import '~assets/styles/themes/red-dark.scss';
@import './assets/styles/themes/red-light.scss'; @import '~assets/styles/themes/red-light.scss';
@import './assets/styles/themes/solarized-dark.scss'; @import '~assets/styles/themes/solarized-dark.scss';
/** Global component style definition */ /** Global component style definition */
@import './assets/styles/global-components-style.scss'; @import '~assets/styles/global-components-style.scss';
/** Import the component-related style sheets here */ /** Import the component-related style sheets here */
@import './app/site/site.component.scss-theme.scss'; @import '~app/site/site.component.scss-theme.scss';
@import './app/shared/components/projector-button/projector-button.component.scss'; @import '~app/shared/components/projector-button/projector-button.component.scss';
@import './app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss'; @import '~app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss';
@import './app/shared/components/sorting-tree/sorting-tree.component.scss'; @import '~app/shared/components/sorting-tree/sorting-tree.component.scss';
@import './app/shared/components/global-spinner/global-spinner.component.scss'; @import '~app/shared/components/global-spinner/global-spinner.component.scss';
@import './app/shared/components/tile/tile.component.scss'; @import '~app/shared/components/tile/tile.component.scss';
@import './app/shared/components/block-tile/block-tile.component.scss'; @import '~app/shared/components/block-tile/block-tile.component.scss';
@import './app/shared/components/icon-container/icon-container.component.scss'; @import '~app/shared/components/icon-container/icon-container.component.scss';
@import './app/site/common/components/start/start.component.scss'; @import '~app/site/common/components/start/start.component.scss';
@import './app/site/mediafiles/components/mediafile-list/mediafile-list.component.scss-theme.scss'; @import '~app/site/mediafiles/components/mediafile-list/mediafile-list.component.scss-theme.scss';
@import './app/site/common/components/super-search/super-search.component.scss'; @import '~app/site/common/components/super-search/super-search.component.scss';
@import './app/shared/components/rounded-input/rounded-input.component.scss'; @import '~app/shared/components/rounded-input/rounded-input.component.scss';
@import './app/shared/components/meta-text-block/meta-text-block.component.scss'; @import '~app/shared/components/meta-text-block/meta-text-block.component.scss';
@import './app/site/config/components/config-field/config-field.component.scss-theme.scss'; @import '~app/site/config/components/config-field/config-field.component.scss-theme.scss';
@import './app/site/motions/modules/motion-detail/components/amendment-create-wizard/amendment-create-wizard.components.scss-theme.scss'; @import '~app/site/motions/modules/motion-detail/components/amendment-create-wizard/amendment-create-wizard.components.scss-theme.scss';
@import './app/site/motions/modules/motion-detail/components/motion-detail-diff/motion-detail-diff.component.scss-theme.scss'; @import '~app/site/motions/modules/motion-detail/components/motion-detail-diff/motion-detail-diff.component.scss-theme.scss';
@import './app/shared/components/banner/banner.component.scss-theme.scss'; @import '~app/shared/components/banner/banner.component.scss-theme.scss';
@import './app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.scss-theme.scss'; @import '~app/site/motions/modules/motion-poll/motion-poll/motion-poll.component.scss-theme.scss';
@import './app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss'; @import '~app/site/motions/modules/motion-poll/motion-poll-detail/motion-poll-detail.component.scss-theme.scss';
@import './app/site/assignments/modules/assignment-poll/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss'; @import '~app/site/assignments/modules/assignment-poll/components/assignment-poll-detail/assignment-poll-detail-component.scss-theme.scss';
@import './app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss'; @import '~app/shared/components/progress-snack-bar/progress-snack-bar.component.scss-theme.scss';
@import './app/shared/components/jitsi/jitsi.component.scss-theme.scss'; @import '~app/shared/components/jitsi/jitsi.component.scss-theme.scss';
@import './app/shared/components/list-view-table/list-view-table.component.scss-theme.scss'; @import '~app/shared/components/list-view-table/list-view-table.component.scss-theme.scss';
@import './app/site/common/components/user-statistics/user-statistics.component.scss-theme.scss'; @import '~app/site/common/components/user-statistics/user-statistics.component.scss-theme.scss';
@import './app/site/login/components/login-wrapper/login-wrapper.component.scss-theme.scss'; @import '~app/site/login/components/login-wrapper/login-wrapper.component.scss-theme.scss';
@import '~app/shared/components/progress/progress.component.scss-theme.scss';
/** Mix the component-related style-rules. Every single custom style goes here */ /** Mix the component-related style-rules. Every single custom style goes here */
@mixin openslides-components-theme($theme) { @mixin openslides-components-theme($theme) {
@ -72,6 +73,7 @@ $narrow-spacing: (
@include os-list-view-table-theme($theme); @include os-list-view-table-theme($theme);
@include os-user-statistics-style($theme); @include os-user-statistics-style($theme);
@include os-login-wrapper-theme($theme); @include os-login-wrapper-theme($theme);
@include os-progress-theme($theme);
} }
.openslides-default-light-theme { .openslides-default-light-theme {
@ -126,7 +128,7 @@ $narrow-spacing: (
/** /**
* Custom configuration for light themes * Custom configuration for light themes
*/ */
[class^="openslides-"][class*="-light-theme"] { [class^='openslides-'][class*='-light-theme'] {
.logo-container { .logo-container {
img.dark { img.dark {
display: none; display: none;
@ -140,7 +142,7 @@ $narrow-spacing: (
/** /**
* Custom configuration for dark themes * Custom configuration for dark themes
*/ */
[class^="openslides-"][class*="-dark-theme"] { [class^='openslides-'][class*='-dark-theme'] {
color: white; color: white;
.logo-container { .logo-container {
img.dark { img.dark {

View File

@ -1,4 +1,4 @@
@import './assets/styles/color-palettes/os-blue'; @import '.~assets/styles/color-palettes/os-blue';
$openslides-primary: mat-palette($openslides-blue); $openslides-primary: mat-palette($openslides-blue);
$openslides-accent: mat-palette($mat-light-blue); $openslides-accent: mat-palette($mat-light-blue);

View File

@ -1,4 +1,4 @@
@import './assets/styles/color-palettes/os-blue'; @import '~assets/styles/color-palettes/os-blue';
// Generate paletes using: https://material.io/design/color/ // Generate paletes using: https://material.io/design/color/
// default values for mat-palette: $default: 500, $lighter: 100, $darker: 700. // default values for mat-palette: $default: 500, $lighter: 100, $darker: 700.

View File

@ -1,4 +1,4 @@
@import './assets/styles/color-palettes/os-green.scss'; @import '~assets/styles/color-palettes/os-green.scss';
$openslides-primary: mat-palette($openslides-green); $openslides-primary: mat-palette($openslides-green);
$openslides-accent: mat-palette($mat-amber); $openslides-accent: mat-palette($mat-amber);

View File

@ -1,4 +1,4 @@
@import './assets/styles/color-palettes/os-green.scss'; @import '~assets/styles/color-palettes/os-green.scss';
$openslides-primary: mat-palette($openslides-green); $openslides-primary: mat-palette($openslides-green);
$openslides-accent: mat-palette($mat-amber); $openslides-accent: mat-palette($mat-amber);

View File

@ -1,4 +1,4 @@
@import './assets/styles/color-palettes/os-red.scss'; @import '~assets/styles/color-palettes/os-red.scss';
$openslides-primary: mat-palette($openslides-primary-red, 500, 300, 900); $openslides-primary: mat-palette($openslides-primary-red, 500, 300, 900);
$openslides-accent: mat-palette($mat-amber); $openslides-accent: mat-palette($mat-amber);

View File

@ -1,4 +1,4 @@
@import './assets/styles/color-palettes/os-red.scss'; @import '~assets/styles/color-palettes/os-red.scss';
$openslides-primary: mat-palette($openslides-primary-red, 500, 300, 900); $openslides-primary: mat-palette($openslides-primary-red, 500, 300, 900);
$openslides-accent: mat-palette($mat-amber); $openslides-accent: mat-palette($mat-amber);

View File

@ -1,5 +1,5 @@
@import './assets/styles/color-palettes/os-cyan.scss'; @import '~assets/styles/color-palettes/os-cyan.scss';
@import './assets/styles/color-palettes/os-gray.scss'; @import '~assets/styles/color-palettes/os-gray.scss';
$openslides-primary: mat-palette($openslides-grey); $openslides-primary: mat-palette($openslides-grey);
$openslides-accent: mat-palette($openslides-cyan); $openslides-accent: mat-palette($openslides-cyan);

View File

@ -0,0 +1,226 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg2"
width="24"
height="24"
viewBox="0 0 24 24"
sodipodi:docname="clapping_hands.svg"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07)">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
<linearGradient
inkscape:collect="always"
id="linearGradient1782">
<stop
style="stop-color:#0c2c40;stop-opacity:1"
offset="0"
id="stop1778" />
<stop
style="stop-color:#2ca2ec;stop-opacity:1"
offset="1"
id="stop1780" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient1722">
<stop
style="stop-color:#f56055;stop-opacity:1;"
offset="0"
id="stop1718" />
<stop
style="stop-color:#f56055;stop-opacity:0;"
offset="1"
id="stop1720" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient1505">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop1501" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop1503" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1505"
id="linearGradient1507"
x1="-72.521545"
y1="-354.46579"
x2="-72.521545"
y2="-220.85248"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(4.5351563e-5,0.25821471)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1722"
id="linearGradient1724"
x1="1637.6523"
y1="687.29828"
x2="1637.6523"
y2="726.80103"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,1.708266,4.6566406e-4,-480.80936)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient1782"
id="radialGradient1784"
cx="773.6098"
cy="-57.234745"
fx="773.6098"
fy="-57.234745"
r="507.56699"
gradientTransform="matrix(0.78017444,0,0,0.14366746,326.05843,630.13805)"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1080"
id="namedview4"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:snap-bbox="true"
inkscape:bbox-paths="true"
inkscape:bbox-nodes="true"
inkscape:snap-bbox-edge-midpoints="true"
inkscape:snap-bbox-midpoints="true"
inkscape:object-paths="true"
inkscape:snap-intersection-paths="true"
inkscape:snap-smooth-nodes="true"
inkscape:snap-midpoints="true"
inkscape:snap-object-midpoints="true"
inkscape:zoom="22.627417"
inkscape:cx="19.730628"
inkscape:cy="14.778162"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g2182"
inkscape:snap-global="true"
inkscape:document-rotation="0">
<sodipodi:guide
position="-8.5967379,25.493463"
orientation="1,0"
id="guide1229" />
<sodipodi:guide
position="-5.4316862,28.164944"
orientation="0,-1"
id="guide1231" />
</sodipodi:namedview>
<g
id="g2441"
transform="matrix(0.93524686,0,0,0.93524686,-1220.0337,-836.48596)"
style="fill:#000000;fill-opacity:1">
<g
transform="matrix(0.76088502,0,0,0.76030764,311.54674,218.13833)"
id="g2397"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13311">
<g
id="g1964-4"
transform="rotate(90,218.24928,1114.2299)"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13311">
<g
id="g1948-2"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13311">
<g
id="g1946-2"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13311">
<g
id="g1944-4"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13311">
<path
id="path1942-5"
d="M 23,5.5 V 20 c 0,2.2 -1.8,4 -4,4 H 11.7 C 10.62,24 9.6,23.57 8.85,22.81 L 1,14.83 c 0,0 1.26,-1.23 1.3,-1.25 0.22,-0.19 0.49,-0.29 0.79,-0.29 0.22,0 0.42,0.06 0.6,0.16 C 3.73,13.46 8,15.91 8,15.91 V 4 C 8,3.17 8.67,2.5 9.5,2.5 10.33,2.5 11,3.17 11,4 v 7 h 1 V 1.5 C 12,0.67 12.67,0 13.5,0 14.33,0 15,0.67 15,1.5 V 11 h 1 V 2.5 C 16,1.67 16.67,1 17.5,1 18.33,1 19,1.67 19,2.5 V 11 h 1 V 5.5 C 20,4.67 20.67,4 21.5,4 22.33,4 23,4.67 23,5.5 Z"
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13311" />
</g>
<g
transform="translate(5.4402324e-7,4.0928558e-5)"
id="g1944-4-0"
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1.13311" />
</g>
</g>
</g>
</g>
<g
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.894784;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(1.1175883,0,0,1.1175883,-155.61227,-104.47506)"
id="g2182">
<g
id="g860"
style="opacity:1;stroke-width:0.855545"
transform="matrix(1.0458649,0,0,1.0458649,-60.689016,-41.511804)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.855541;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 1326.2009,898.5852 -2.6143,2.61248 c 0.054,0.0477 0.1074,0.097 0.1593,0.14738 0.05,0.0525 0.099,0.10627 0.1459,0.16112 l 2.6168,-2.61488 z m -4.6234,-2.21282 -0.434,-3e-5 v 3.69727 c 0.1446,-0.006 0.2894,-0.006 0.434,4e-5 z m 7.1449,6.80056 -3.7002,3e-5 c 0.013,0.14447 0.013,0.28922 2e-4,0.43368 l 3.7,-2e-5 z"
id="path862" />
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.855541;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 1321.1426,895.92578 a 0.44743473,0.44743473 0 0 0 -0.4473,0.44727 v 3.69726 a 0.44743473,0.44743473 0 0 0 0.4668,0.44727 c 0.1322,-0.005 0.2643,-0.006 0.3965,0 a 0.44743473,0.44743473 0 0 0 0.4668,-0.44727 v -3.69726 a 0.44743473,0.44743473 0 0 0 -0.4473,-0.44727 z m 5.0703,2.21289 a 0.44743473,0.44743473 0 0 0 -0.3281,0.13086 l -2.6153,2.61133 a 0.44743473,0.44743473 0 0 0 0.021,0.65234 c 0.049,0.0433 0.096,0.0877 0.1426,0.13282 0.041,0.0437 0.082,0.0873 0.1191,0.13086 a 0.44743473,0.44743473 0 0 0 0.6563,0.0254 l 2.6152,-2.61524 a 0.44743473,0.44743473 0 0 0 0,-0.63281 l -0.3086,-0.30664 a 0.44743473,0.44743473 0 0 0 -0.3027,-0.12891 z m -1.1914,4.58594 a 0.44743473,0.44743473 0 0 0 -0.4453,0.48828 c 0.011,0.1179 0.011,0.23523 0,0.35352 a 0.44743473,0.44743473 0 0 0 0.4453,0.48828 h 3.7012 a 0.44743473,0.44743473 0 0 0 0.4472,-0.44727 v -0.43359 a 0.44743473,0.44743473 0 0 0 -0.4472,-0.44922 z"
id="path864" />
</g>
</g>
<g
transform="matrix(0.86183474,0,0,0.86183474,180.09301,126.9712)"
id="g2397-4"
style="fill:#000000;fill-opacity:1">
<g
id="g1964-4-5"
transform="rotate(90,218.24928,1114.2299)"
style="fill:#000000;fill-opacity:1">
<g
id="g1948-2-0"
style="fill:#000000;fill-opacity:1">
<g
id="g1946-2-57"
style="fill:#000000;fill-opacity:1">
<g
id="g1944-4-32"
style="fill:#000000;fill-opacity:1" />
<g
transform="translate(5.4402324e-7,4.0928558e-5)"
id="g1944-4-0-2"
style="fill:#000000;fill-opacity:1" />
</g>
</g>
</g>
</g>
</g>
<path
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.806028"
d="M 6.5569914,2.9864148 0.87698077,8.56734 C 0.33613684,9.1006455 0.03031238,9.8273997 0.03031238,10.595361 v 5.189794 c 0,1.181491 0.73189623,2.199492 1.76396712,2.62764 -0.00888,-0.08924 -0.02737,-0.174463 -0.02737,-0.265356 v -5.191514 c 0,-0.955146 0.3829864,-1.864797 1.0569667,-2.529425 L 6.3743238,6.9356205 C 6.8910656,6.0338659 7.5323796,4.9178609 7.538295,4.8989906 7.6098007,4.7710011 7.6521007,4.6281034 7.6521007,4.4716724 7.6521007,4.2583484 7.5805947,4.066391 7.4451576,3.90996 7.4310282,3.8814651 6.5571713,2.9864148 6.5571713,2.9864148 Z m 3.8401146,4.9778558 c -0.02737,0.048433 -0.0459,0.080279 -0.07769,0.1361228 C 10.154314,8.3899921 9.9344832,8.7732978 9.7158851,9.1548918 9.5822184,9.3881797 9.5695934,9.4091143 9.4485527,9.6201187 h 5.6800103 c 0.116884,-0.1710937 0.201821,-0.3659799 0.201821,-0.5892814 0,-0.5901895 -0.476748,-1.0665667 -1.067387,-1.0665667 z"
id="path1942-5-3" />
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,18 +1,18 @@
/** theming */ /** theming */
@import './assets/styles/component-themes.scss'; @import '~assets/styles/component-themes.scss';
/** fonts */ /** fonts */
@import './assets/styles/fonts.scss'; @import '~assets/styles/fonts.scss';
@import '~material-icon-font/dist/Material-Icons.css'; @import '~material-icon-font/dist/Material-Icons.css';
/** Videojs */ /** Videojs */
@import '~video.js/dist/video-js.css'; @import '~video.js/dist/video-js.css';
/** Load projector specific SCSS values */ /** Load projector specific SCSS values */
@import './assets/styles/projector.scss'; @import '~assets/styles/projector.scss';
/** Load global scss variables and device mixing */ /** Load global scss variables and device mixing */
@import './assets/styles/variables.scss'; @import '~assets/styles/variables.scss';
.pbl-ngrid-cell { .pbl-ngrid-cell {
.fill { .fill {
@ -788,3 +788,7 @@ button.mat-menu-item.selected {
.hide { .hide {
display: none; display: none;
} }
.hide-height {
height: 0 !important;
}

View File

@ -15,7 +15,6 @@ RUN apt-get -y update && apt-get install --no-install-recommends -y \
libxmlsec1-dev \ libxmlsec1-dev \
libxmlsec1-openssl \ libxmlsec1-openssl \
pkg-config pkg-config
RUN rm -rf /var/lib/apt/lists/* RUN rm -rf /var/lib/apt/lists/*
COPY requirements /app/requirements COPY requirements /app/requirements

View File

@ -169,6 +169,85 @@ def get_config_variables():
subgroup="Live conference", subgroup="Live conference",
) )
# Applause
yield ConfigVariable(
name="general_system_applause_enable",
default_value=False,
input_type="boolean",
label="Enable virtual applause",
help_text="Shows a 'Send applause' icon in the live stream bar",
weight=170,
subgroup="Virtual applause",
)
yield ConfigVariable(
name="general_system_applause_type",
default_value="applause-type-bar",
input_type="choice",
choices=(
{
"value": "applause-type-bar",
"display_name": "Bar",
},
{
"value": "applause-type-particles",
"display_name": "Particles",
},
),
label="Applause Type",
weight=171,
subgroup="Virtual applause",
)
yield ConfigVariable(
name="general_system_applause_show_level",
default_value=False,
input_type="boolean",
label="Show applause amount",
weight=172,
subgroup="Virtual applause",
)
yield ConfigVariable(
name="general_system_applause_min_amount",
default_value=1,
input_type="integer",
label="Lowest applause amount",
help_text="Lowest amount required for OpenSlides to recognize applause",
weight=173,
subgroup="Virtual applause",
)
yield ConfigVariable(
name="general_system_applause_max_amount",
default_value=0,
input_type="integer",
label="Highest applause amount",
help_text="Defines the maximum deflection of the amount. Entering zero will use the amount of present users instead.",
weight=174,
subgroup="Virtual applause",
)
yield ConfigVariable(
name="general_system_stream_applause_timeout",
default_value=5,
input_type="integer",
label="Applause timeout in seconds",
help_text="Determines how long a user has to wait to applaud again. Also determines the time in which applause is collected",
weight=175,
subgroup="Virtual applause",
)
yield ConfigVariable(
name="general_system_applause_particle_image",
default_value="",
label="Applause particle image url",
help_text="Shows the given image as applause particle. Recommended image format: 24x24px, PNG, JPG or SVG",
weight=176,
subgroup="Virtual applause",
)
# General System # General System
yield ConfigVariable( yield ConfigVariable(