Merge pull request #4578 from GabrielInTheWorld/theming
Implements a mechanism for a fallback theme
This commit is contained in:
commit
51b4b6aba6
@ -1,3 +1,4 @@
|
||||
<div class="content">
|
||||
<router-outlet></router-outlet>
|
||||
<os-global-spinner></os-global-spinner>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@ describe('AppComponent', () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.debugElement.componentInstance;
|
||||
expect(app).toBeTruthy();
|
||||
tick();
|
||||
tick(1000);
|
||||
fixture.whenStable().then(() => {
|
||||
expect(servertimeService.startScheduler).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Component, ApplicationRef } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { take, filter } from 'rxjs/operators';
|
||||
import { take, filter, auditTime } from 'rxjs/operators';
|
||||
|
||||
import { ConfigService } from './core/ui-services/config.service';
|
||||
import { ConstantsService } from './core/core-services/constants.service';
|
||||
@ -15,6 +15,8 @@ import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade
|
||||
import { UpdateService } from './core/ui-services/update.service';
|
||||
import { PrioritizeService } from './core/core-services/prioritize.service';
|
||||
import { PingService } from './core/core-services/ping.service';
|
||||
import { SpinnerService } from './core/ui-services/spinner.service';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
/**
|
||||
* Angular's global App Component
|
||||
@ -48,10 +50,12 @@ export class AppComponent {
|
||||
translate: TranslateService,
|
||||
appRef: ApplicationRef,
|
||||
servertimeService: ServertimeService,
|
||||
router: Router,
|
||||
operator: OperatorService,
|
||||
loginDataService: LoginDataService,
|
||||
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
|
||||
themeService: ThemeService,
|
||||
spinnerService: SpinnerService,
|
||||
countUsersService: CountUsersService, // Needed to register itself.
|
||||
configService: ConfigService,
|
||||
loadFontService: LoadFontService,
|
||||
@ -70,13 +74,27 @@ export class AppComponent {
|
||||
translate.use(translate.getLangs().includes(browserLang) ? browserLang : 'en');
|
||||
// change default JS functions
|
||||
this.overloadArrayToString();
|
||||
// Show the spinner initial
|
||||
spinnerService.setVisibility(true, translate.instant('Loading data. Please wait...'));
|
||||
|
||||
appRef.isStable
|
||||
.pipe(
|
||||
// take only the stable state
|
||||
filter(s => s),
|
||||
take(1)
|
||||
)
|
||||
.subscribe(() => servertimeService.startScheduler());
|
||||
|
||||
// Subscribe to hide the spinner if the application has changed.
|
||||
appRef.isStable
|
||||
.pipe(
|
||||
filter(s => s),
|
||||
auditTime(1000)
|
||||
)
|
||||
.pipe(take(2))
|
||||
.subscribe(() => {
|
||||
spinnerService.setVisibility(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -17,6 +17,7 @@ import { OpenSlidesTranslateModule } from './core/translate/openslides-translate
|
||||
// PWA
|
||||
import { ServiceWorkerModule } from '@angular/service-worker';
|
||||
import { environment } from '../environments/environment';
|
||||
import { GlobalSpinnerComponent } from './site/global-spinner/global-spinner.component';
|
||||
|
||||
/**
|
||||
* Returns a function that returns a promis that will be resolved, if all apps are loaded.
|
||||
@ -30,7 +31,7 @@ export function AppLoaderFactory(appLoadService: AppLoadService): () => Promise<
|
||||
* Global App Module. Keep it as clean as possible.
|
||||
*/
|
||||
@NgModule({
|
||||
declarations: [AppComponent],
|
||||
declarations: [AppComponent, GlobalSpinnerComponent],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
HttpClientModule,
|
||||
|
@ -13,6 +13,10 @@ export interface LoginData {
|
||||
privacy_policy: string;
|
||||
legal_notice: string;
|
||||
theme: string;
|
||||
logo_web_header: {
|
||||
path: string;
|
||||
display_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
const LOGIN_DATA_STORAGE_KEY = 'LoginData';
|
||||
@ -61,6 +65,21 @@ export class LoginDataService {
|
||||
return this._theme.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds the custom web header
|
||||
*/
|
||||
private readonly _logo_web_header = new BehaviorSubject<{ path: string; display_name: string }>({
|
||||
path: '',
|
||||
display_name: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns an observable for the web header
|
||||
*/
|
||||
public get logo_web_header(): Observable<{ path: string; display_name: string }> {
|
||||
return this._logo_web_header.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs this service. The config service is needed to update the privacy
|
||||
* policy and legal notice, when their config values change.
|
||||
@ -83,6 +102,10 @@ export class LoginDataService {
|
||||
this._theme.next(value);
|
||||
this.storeLoginData();
|
||||
});
|
||||
configService.get<{ path: string; display_name: string }>('logo_web_header').subscribe(value => {
|
||||
this._logo_web_header.next(value);
|
||||
this.storeLoginData();
|
||||
});
|
||||
|
||||
this.loadLoginData();
|
||||
}
|
||||
@ -120,7 +143,8 @@ export class LoginDataService {
|
||||
loginData = {
|
||||
privacy_policy: this._privacy_policy.getValue(),
|
||||
legal_notice: this._legal_notice.getValue(),
|
||||
theme: this._theme.getValue()
|
||||
theme: this._theme.getValue(),
|
||||
logo_web_header: this._logo_web_header.getValue()
|
||||
};
|
||||
}
|
||||
if (!this.OSStatus.isInHistoryMode) {
|
||||
|
12
client/src/app/core/ui-services/spinner.service.spec.ts
Normal file
12
client/src/app/core/ui-services/spinner.service.spec.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { SpinnerService } from './spinner.service';
|
||||
|
||||
describe('SpinnerService', () => {
|
||||
beforeEach(() => TestBed.configureTestingModule({}));
|
||||
|
||||
it('should be created', () => {
|
||||
const service: SpinnerService = TestBed.get(SpinnerService);
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
40
client/src/app/core/ui-services/spinner.service.ts
Normal file
40
client/src/app/core/ui-services/spinner.service.ts
Normal file
@ -0,0 +1,40 @@
|
||||
// External imports
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, Subject } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Service for the `global-spinner.component`
|
||||
*
|
||||
* Handles the visibility of the global-spinner.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SpinnerService {
|
||||
/**
|
||||
* Subject, that holds the visibility and message. The component can observe this.
|
||||
*/
|
||||
private visibility: Subject<{ isVisible: boolean; text?: string }> = new Subject<{
|
||||
isVisible: boolean;
|
||||
text?: string;
|
||||
}>();
|
||||
|
||||
/**
|
||||
* Function to change the visibility of the `global-spinner.component`.
|
||||
*
|
||||
* @param isVisible flag, if the spinner should be shown.
|
||||
* @param text optional. If the spinner should show a message.
|
||||
*/
|
||||
public setVisibility(isVisible: boolean, text?: string): void {
|
||||
setTimeout(() => this.visibility.next({ isVisible, text }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to get the visibility as observable.
|
||||
*
|
||||
* @returns class member `visibility`.
|
||||
*/
|
||||
public getVisibility(): Observable<{ isVisible: boolean; text?: string }> {
|
||||
return this.visibility;
|
||||
}
|
||||
}
|
@ -9,6 +9,26 @@ import { LoginDataService } from './login-data.service';
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ThemeService {
|
||||
/**
|
||||
* Constant, that describes the default theme class.
|
||||
*/
|
||||
public static DEFAULT_THEME = 'openslides-theme';
|
||||
|
||||
/**
|
||||
* Constant path of the logo with dark colors for bright themes.
|
||||
*/
|
||||
public static STANDARD_LOGO = '/assets/img/openslides-logo-h.svg';
|
||||
|
||||
/**
|
||||
* Constant path of the logo with white colors for dark themes.
|
||||
*/
|
||||
public static STANDARD_LOGO_DARK_THEME = '/assets/img/openslides-logo-h-dark-transparent.svg';
|
||||
|
||||
/**
|
||||
* Holds the current theme as member.
|
||||
*/
|
||||
private currentTheme: string;
|
||||
|
||||
/**
|
||||
* Here it will subscribe to the observer from login data service. The stheme is part of
|
||||
* the login data, so get it from there and not from the config. This service will
|
||||
@ -21,13 +41,31 @@ export class ThemeService {
|
||||
if (!newTheme) {
|
||||
return;
|
||||
}
|
||||
this.currentTheme = newTheme;
|
||||
|
||||
const classList = document.getElementsByTagName('body')[0].classList; // Get the classlist of the body.
|
||||
const toRemove = Array.from(classList).filter((item: string) => item.includes('theme'));
|
||||
if (toRemove.length) {
|
||||
classList.remove(...toRemove); // Remove all old themes.
|
||||
}
|
||||
classList.add(newTheme); // Add the new theme.
|
||||
classList.add(newTheme, ThemeService.DEFAULT_THEME); // Add the new theme.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the logo relative to the used theme.
|
||||
*
|
||||
* @param shouldDefault If this method should return the default logo.
|
||||
*
|
||||
* @returns the path to the logo.
|
||||
*/
|
||||
public getLogoRelativeToTheme(shouldDefault?: boolean): string {
|
||||
if (this.currentTheme) {
|
||||
return this.currentTheme.includes('dark') && !shouldDefault
|
||||
? ThemeService.STANDARD_LOGO_DARK_THEME
|
||||
: ThemeService.STANDARD_LOGO;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
<div *ngIf="getImage() != ''" class="logo-container">
|
||||
<img [src]="getImage()">
|
||||
<div *ngIf="getImage()" class="logo-container">
|
||||
<img [src]="getImage('true')" class="default" />
|
||||
<img [src]="getImage()" class="dark"/>
|
||||
</div>
|
||||
|
@ -2,6 +2,7 @@ img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
max-height: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
|
@ -1,63 +1,27 @@
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
|
||||
|
||||
import { MediaManageService } from 'app/core/ui-services/media-manage.service';
|
||||
import { ConfigService } from 'app/core/ui-services/config.service';
|
||||
import { ThemeService } from 'app/core/ui-services/theme.service';
|
||||
import { LoginDataService } from 'app/core/ui-services/login-data.service';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
/**
|
||||
* Reusable Logo component for Apps.
|
||||
*
|
||||
* Following actions are possible:
|
||||
* * "logo_projector_main"
|
||||
* * "logo_projector_header"
|
||||
* * "logo_web_header"
|
||||
* * "logo_pdf_header_L"
|
||||
* * "logo_pdf_header_R"
|
||||
* * "logo_pdf_footer_L"
|
||||
* * "logo_pdf_footer_R"
|
||||
* * "logo_pdf_ballot_paper"
|
||||
*
|
||||
* ## Examples:
|
||||
*
|
||||
* ### Usage of the selector:
|
||||
*
|
||||
* ```html
|
||||
* <os-logo
|
||||
* inputAction="logo_projector_main"
|
||||
* [footer]="false"
|
||||
* [alignment]="right">
|
||||
* </os-logo>
|
||||
* ```
|
||||
*
|
||||
* Sidenote: The footer variable is optional. Only if you want
|
||||
* alternating logos, i.E. in the sidenav. the Alignment is also
|
||||
* optional.
|
||||
* Component to hold the logo for the app.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-logo',
|
||||
templateUrl: './logo.component.html',
|
||||
styleUrls: ['./logo.component.scss']
|
||||
})
|
||||
export class LogoComponent implements OnInit {
|
||||
export class LogoComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Constant path of the logo with dark colors for bright themes
|
||||
* Local variable to hold the path for a custom web header.
|
||||
*/
|
||||
public static STANDARD_LOGO = '/assets/img/openslides-logo-h.svg';
|
||||
private logoPath: string;
|
||||
|
||||
/**
|
||||
* Constant path of the logo with white colors for dark themes
|
||||
* Local variable to hold the subscription to unsubscribe if exists.
|
||||
*/
|
||||
public static STANDARD_LOGO_DARK_THEME = '/assets/img/openslides-logo-h-dark-transparent.svg';
|
||||
|
||||
/**
|
||||
* Holds the actions for logos. Updated via an observable
|
||||
*/
|
||||
public logoActions: string[];
|
||||
|
||||
/**
|
||||
* decides based on the actionString how to display the logo
|
||||
*/
|
||||
@Input()
|
||||
public inputAction: string;
|
||||
private logoSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* determines if the current picture is displayed in the footer.
|
||||
@ -66,117 +30,46 @@ export class LogoComponent implements OnInit {
|
||||
@Input()
|
||||
public footer = false;
|
||||
|
||||
/**
|
||||
* influences text-alignment in the .logo-container css class
|
||||
*/
|
||||
@Input()
|
||||
public alignment = 'center';
|
||||
/**
|
||||
* The constructor
|
||||
*
|
||||
* @param mmservice The Media Manage Service
|
||||
* @param configService The ConfigService to subscribe to theme-changes
|
||||
* @param loginDataService Reference to the `LoginDataService`
|
||||
* @param themeService Reference to the `ThemeService`
|
||||
*/
|
||||
public constructor(private mmservice: MediaManageService, private configService: ConfigService) {}
|
||||
public constructor(private loginDataService: LoginDataService, private themeService: ThemeService) {}
|
||||
|
||||
/**
|
||||
* Initialization function
|
||||
* On init method
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.mmservice.getLogoActions().subscribe(action => {
|
||||
this.logoActions = action;
|
||||
this.logoSubscription = this.loginDataService.logo_web_header.subscribe(nextLogo => {
|
||||
if (nextLogo) {
|
||||
this.logoPath = nextLogo.path;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the image based on the inputAction and location.
|
||||
* Possible inputActions are in the class description.
|
||||
* On destroy method
|
||||
*/
|
||||
public ngOnDestroy(): void {
|
||||
if (this.logoSubscription) {
|
||||
this.logoSubscription.unsubscribe();
|
||||
this.logoSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image based on custom images and footer.
|
||||
* If a custom image is set and this component is displayed as footer or there is no custom image, then the OpenSlides logo is used.
|
||||
*
|
||||
* @returns path to image
|
||||
*/
|
||||
public getImage(): string {
|
||||
if (this.footer) {
|
||||
const path = this.getFooterImage(this.inputAction);
|
||||
return path;
|
||||
public getImage(shouldDefault?: boolean): string {
|
||||
if ((!this.logoPath && !this.footer) || (!!this.logoPath && this.footer)) {
|
||||
return this.themeService.getLogoRelativeToTheme(shouldDefault);
|
||||
} else {
|
||||
const path = this.getHeaderImage(this.inputAction, this.alignment);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user uses a dark theme or a 'bright' theme.
|
||||
* In relation to the theme this will return the corresponding imagepath.
|
||||
*
|
||||
* @returns path of the image corresponding to the chosen theme.
|
||||
*/
|
||||
protected getImagePathRelatedToTheme(): string {
|
||||
const theme = this.configService.instant<string>('openslides_theme');
|
||||
if (theme) {
|
||||
return theme.includes('dark') ? LogoComponent.STANDARD_LOGO_DARK_THEME : LogoComponent.STANDARD_LOGO;
|
||||
} else {
|
||||
return LogoComponent.STANDARD_LOGO;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* gets the header image based on logo action
|
||||
*
|
||||
* @param logoAction the logo action to be used
|
||||
* @param alignment the alignment of the logo (optional)
|
||||
* @returns path to image
|
||||
*/
|
||||
protected getHeaderImage(logoAction: string, alignment: string = 'center'): string {
|
||||
if (alignment !== 'center') {
|
||||
this.setAlignment(alignment);
|
||||
}
|
||||
let path = '';
|
||||
/* check if datastore is loaded and custom logo can be read */
|
||||
if (this.logoActions === undefined) {
|
||||
return '';
|
||||
}
|
||||
if (this.mmservice !== undefined) {
|
||||
if (this.mmservice.isImageConfigObject(this.mmservice.getMediaConfig(logoAction))) {
|
||||
const imageConfig = this.mmservice.getMediaConfig(logoAction);
|
||||
path = imageConfig.path;
|
||||
}
|
||||
}
|
||||
if (path === '') {
|
||||
path = this.getImagePathRelatedToTheme();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the alignment from center to either 'left' or 'right'
|
||||
*
|
||||
* @param alignment either 'right' or 'left'
|
||||
*/
|
||||
private setAlignment(alignment: string): void {
|
||||
if (alignment === 'left' || alignment === 'right') {
|
||||
const cssLogoContainer = document.getElementsByClassName('logo-container') as HTMLCollectionOf<HTMLElement>;
|
||||
if (cssLogoContainer.length !== 0) {
|
||||
cssLogoContainer[0].style.textAlign = alignment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the image-path for the footer
|
||||
*
|
||||
* @param logoAction the logo action to be used
|
||||
* @returns '' if no logo is set and path to standard logo if a custom
|
||||
* logo was set
|
||||
*/
|
||||
protected getFooterImage(logoAction: string): string {
|
||||
if (
|
||||
this.getHeaderImage(logoAction) === LogoComponent.STANDARD_LOGO ||
|
||||
this.getHeaderImage(logoAction) === LogoComponent.STANDARD_LOGO_DARK_THEME ||
|
||||
this.getHeaderImage(logoAction) === ''
|
||||
) {
|
||||
return '';
|
||||
} else {
|
||||
return this.getImagePathRelatedToTheme();
|
||||
return this.logoPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
<div
|
||||
*ngIf="isVisible"
|
||||
class="global-spinner-component">
|
||||
<div class="spinner-container">
|
||||
<div>
|
||||
<div class="spinner"></div>
|
||||
<div class="text">{{ text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="backdrop"></div>
|
||||
</div>
|
@ -0,0 +1,83 @@
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@mixin os-global-spinner-theme($theme) {
|
||||
$primary: map-get($theme, primary);
|
||||
$accent: map-get($theme, accent);
|
||||
$warn: map-get($theme, warn);
|
||||
$background: map-get($theme, background);
|
||||
$foreground: map-get($theme, foreground);
|
||||
|
||||
$contrast-primary: map-get($primary, contrast);
|
||||
$contrast-accent: map-get($accent, contrast);
|
||||
|
||||
.global-spinner-component,
|
||||
.backdrop,
|
||||
.spinner-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.global-spinner-component {
|
||||
position: fixed;
|
||||
|
||||
.spinner-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin: -136px 0 0 -53px;
|
||||
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
border: 6px solid #000;
|
||||
border-radius: 100%;
|
||||
opacity: 0.2;
|
||||
|
||||
animation: rotation 1s infinite linear;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
left: -6px;
|
||||
|
||||
content: '';
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-radius: 100%;
|
||||
border-style: solid;
|
||||
border-width: 6px;
|
||||
border-color: white transparent transparent;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
color: white;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
.backdrop {
|
||||
z-index: 899;
|
||||
background-color: #303030;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { GlobalSpinnerComponent } from './global-spinner.component';
|
||||
|
||||
describe('GlobalSpinnerComponent', () => {
|
||||
let component: GlobalSpinnerComponent;
|
||||
let fixture: ComponentFixture<GlobalSpinnerComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [GlobalSpinnerComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GlobalSpinnerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,74 @@
|
||||
// External imports
|
||||
import { Component, OnInit, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
||||
// Internal imports
|
||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
|
||||
/**
|
||||
* Component for the global spinner.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'os-global-spinner',
|
||||
templateUrl: './global-spinner.component.html',
|
||||
styleUrls: ['./global-spinner.component.scss']
|
||||
})
|
||||
export class GlobalSpinnerComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Text, which will be shown if the spinner is shown.
|
||||
*/
|
||||
public text: string;
|
||||
|
||||
/**
|
||||
* Flag, that defines when the spinner is shown.
|
||||
*/
|
||||
public isVisible = false;
|
||||
|
||||
/**
|
||||
* Subscription for the service to handle the visibility and text for the spinner.
|
||||
*/
|
||||
private spinnerSubscription: Subscription;
|
||||
|
||||
/**
|
||||
* Constant string as default message when the spinner is shown.
|
||||
*/
|
||||
private LOADING = 'Loading data. Please wait...';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param spinnerService Reference to the service for this spinner.
|
||||
* @param translate Service to get translations for the messages.
|
||||
* @param detector Service to manual initiate a change of the UI.
|
||||
*/
|
||||
public constructor(private spinnerService: SpinnerService, private detector: ChangeDetectorRef) {}
|
||||
|
||||
/**
|
||||
* Init method
|
||||
*/
|
||||
public ngOnInit(): void {
|
||||
this.spinnerSubscription = this.spinnerService // subscribe to the service.
|
||||
.getVisibility()
|
||||
.subscribe((value: { isVisible: boolean; text: string }) => {
|
||||
this.isVisible = value.isVisible;
|
||||
this.text = value.text;
|
||||
|
||||
if (!this.text) {
|
||||
this.text = this.LOADING;
|
||||
}
|
||||
this.detector.detectChanges();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy method
|
||||
*
|
||||
* Deletes the subscription and marks the spinner as invisible.
|
||||
*/
|
||||
public ngOnDestroy(): void {
|
||||
if (this.spinnerSubscription) {
|
||||
this.spinnerSubscription.unsubscribe();
|
||||
this.isVisible = false;
|
||||
}
|
||||
this.spinnerSubscription = null;
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
<div class="form-wrapper">
|
||||
<!-- Spinner -->
|
||||
<mat-spinner *ngIf="inProcess"></mat-spinner>
|
||||
|
||||
<!-- Install notice -->
|
||||
<div class="login-container" *ngIf="installationNotice">
|
||||
|
@ -13,6 +13,7 @@ import { environment } from 'environments/environment';
|
||||
import { LoginDataService, LoginData } from 'app/core/ui-services/login-data.service';
|
||||
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
|
||||
import { HttpService } from 'app/core/core-services/http.service';
|
||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
|
||||
interface LoginDataWithInfoText extends LoginData {
|
||||
info_text?: string;
|
||||
@ -54,11 +55,6 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
||||
*/
|
||||
public parentErrorStateMatcher = new ParentErrorStateMatcher();
|
||||
|
||||
/**
|
||||
* Show the Spinner if validation is in process
|
||||
*/
|
||||
public inProcess = false;
|
||||
|
||||
public operatorSubscription: Subscription | null;
|
||||
|
||||
/**
|
||||
@ -71,6 +67,7 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
||||
* @param httpService used to get information before the login
|
||||
* @param OpenSlides The Service for OpenSlides
|
||||
* @param loginDataService provide information about the legal notice and privacy policy
|
||||
* @param spinnerService Service to show the spinner when the user is signing in
|
||||
*/
|
||||
public constructor(
|
||||
title: Title,
|
||||
@ -81,9 +78,12 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
||||
private route: ActivatedRoute,
|
||||
private formBuilder: FormBuilder,
|
||||
private httpService: HttpService,
|
||||
private loginDataService: LoginDataService
|
||||
private loginDataService: LoginDataService,
|
||||
private spinnerService: SpinnerService
|
||||
) {
|
||||
super(title, translate);
|
||||
// Hide the spinner if the user is at `login-mask`
|
||||
spinnerService.setVisibility(false);
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
@ -149,8 +149,8 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
||||
* Send username and password to the {@link AuthService}
|
||||
*/
|
||||
public async formLogin(): Promise<void> {
|
||||
this.spinnerService.setVisibility(true, this.translate.instant('Loading data. Please wait...'));
|
||||
this.loginErrorMsg = '';
|
||||
this.inProcess = true;
|
||||
try {
|
||||
await this.authService.login(this.loginForm.value.username, this.loginForm.value.password, () => {
|
||||
this.clearOperatorSubscription(); // We take control, not the subscription.
|
||||
@ -161,7 +161,7 @@ export class LoginMaskComponent extends BaseComponent implements OnInit, OnDestr
|
||||
});
|
||||
this.loginErrorMsg = e;
|
||||
}
|
||||
this.inProcess = false;
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,6 +18,7 @@ import { TreeService } from 'app/core/ui-services/tree.service';
|
||||
import { UserRepositoryService } from 'app/core/repositories/users/user-repository.service';
|
||||
import { ViewMotion } from '../models/view-motion';
|
||||
import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflow-repository.service';
|
||||
import { SpinnerService } from 'app/core/ui-services/spinner.service';
|
||||
|
||||
/**
|
||||
* Contains all multiselect actions for the motion list view.
|
||||
@ -26,6 +27,8 @@ import { WorkflowRepositoryService } from 'app/core/repositories/motions/workflo
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class MotionMultiselectService {
|
||||
private messageForSpinner = 'Motions are in process. Please wait...';
|
||||
|
||||
/**
|
||||
* Does nothing.
|
||||
*
|
||||
@ -42,6 +45,7 @@ export class MotionMultiselectService {
|
||||
* @param httpService
|
||||
* @param treeService
|
||||
* @param personalNoteService
|
||||
* @param spinnerService to show a spinner when http-requests are made.
|
||||
*/
|
||||
public constructor(
|
||||
private repo: MotionRepositoryService,
|
||||
@ -56,7 +60,8 @@ export class MotionMultiselectService {
|
||||
private motionBlockRepo: MotionBlockRepositoryService,
|
||||
private httpService: HttpService,
|
||||
private treeService: TreeService,
|
||||
private personalNoteService: PersonalNoteService
|
||||
private personalNoteService: PersonalNoteService,
|
||||
private spinnerService: SpinnerService
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -67,9 +72,19 @@ export class MotionMultiselectService {
|
||||
public async delete(motions: ViewMotion[]): Promise<void> {
|
||||
const title = this.translate.instant('Are you sure you want to delete all selected motions?');
|
||||
if (await this.promptService.open(title, null)) {
|
||||
let i = 0;
|
||||
|
||||
for (const motion of motions) {
|
||||
++i;
|
||||
const message =
|
||||
this.translate.instant(this.messageForSpinner) +
|
||||
`\n${i} ` +
|
||||
this.translate.instant('of') +
|
||||
` ${motions.length}`;
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
await this.repo.delete(motion);
|
||||
}
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,7 +117,13 @@ export class MotionMultiselectService {
|
||||
}));
|
||||
const selectedChoice = await this.choiceService.open(title, choices);
|
||||
if (selectedChoice) {
|
||||
await this.repo.setMultiState(motions, selectedChoice.items as number);
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
await this.repo.setMultiState(motions, selectedChoice.items as number).catch(error => {
|
||||
this.spinnerService.setVisibility(false);
|
||||
throw error;
|
||||
});
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,9 +148,18 @@ export class MotionMultiselectService {
|
||||
id: motion.id,
|
||||
recommendation: selectedChoice.action ? 0 : (selectedChoice.items as number)
|
||||
}));
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_recommendation/', {
|
||||
motions: requestData
|
||||
});
|
||||
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
await this.httpService
|
||||
.post('/rest/motions/motion/manage_multiple_recommendation/', {
|
||||
motions: requestData
|
||||
})
|
||||
.catch(error => {
|
||||
this.spinnerService.setVisibility(false);
|
||||
throw error;
|
||||
});
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,12 +179,21 @@ export class MotionMultiselectService {
|
||||
clearChoice
|
||||
);
|
||||
if (selectedChoice) {
|
||||
let i = 0;
|
||||
for (const motion of motions) {
|
||||
++i;
|
||||
const message =
|
||||
this.translate.instant(this.messageForSpinner) +
|
||||
`\n${i} ` +
|
||||
this.translate.instant('of') +
|
||||
` ${motions.length}`;
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
await this.repo.update(
|
||||
{ category_id: selectedChoice.action ? null : (selectedChoice.items as number) },
|
||||
motion
|
||||
);
|
||||
}
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -174,26 +213,33 @@ export class MotionMultiselectService {
|
||||
true,
|
||||
choices
|
||||
);
|
||||
if (selectedChoice && selectedChoice.action === choices[0]) {
|
||||
const requestData = motions.map(motion => {
|
||||
let submitterIds = [...motion.sorted_submitters_id, ...(selectedChoice.items as number[])];
|
||||
submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||
return {
|
||||
id: motion.id,
|
||||
submitters: submitterIds
|
||||
};
|
||||
});
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
||||
} else if (selectedChoice && selectedChoice.action === choices[1]) {
|
||||
const requestData = motions.map(motion => {
|
||||
const submitterIdsToRemove = selectedChoice.items as number[];
|
||||
const submitterIds = motion.sorted_submitters_id.filter(id => !submitterIdsToRemove.includes(id));
|
||||
return {
|
||||
id: motion.id,
|
||||
submitters: submitterIds
|
||||
};
|
||||
});
|
||||
if (selectedChoice) {
|
||||
let requestData = null;
|
||||
if (selectedChoice.action === choices[0]) {
|
||||
requestData = motions.map(motion => {
|
||||
let submitterIds = [...motion.sorted_submitters_id, ...(selectedChoice.items as number[])];
|
||||
submitterIds = submitterIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||
return {
|
||||
id: motion.id,
|
||||
submitters: submitterIds
|
||||
};
|
||||
});
|
||||
// await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
||||
} else if (selectedChoice.action === choices[1]) {
|
||||
requestData = motions.map(motion => {
|
||||
const submitterIdsToRemove = selectedChoice.items as number[];
|
||||
const submitterIds = motion.sorted_submitters_id.filter(id => !submitterIdsToRemove.includes(id));
|
||||
return {
|
||||
id: motion.id,
|
||||
submitters: submitterIds
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_submitters/', { motions: requestData });
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,34 +261,41 @@ export class MotionMultiselectService {
|
||||
true,
|
||||
choices
|
||||
);
|
||||
if (selectedChoice && selectedChoice.action === choices[0]) {
|
||||
const requestData = motions.map(motion => {
|
||||
let tagIds = [...motion.tags_id, ...(selectedChoice.items as number[])];
|
||||
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: tagIds
|
||||
};
|
||||
});
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||
} else if (selectedChoice && selectedChoice.action === choices[1]) {
|
||||
const requestData = motions.map(motion => {
|
||||
const tagIdsToRemove = selectedChoice.items as number[];
|
||||
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: tagIds
|
||||
};
|
||||
});
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||
} else if (selectedChoice && selectedChoice.action === choices[2]) {
|
||||
const requestData = motions.map(motion => {
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: []
|
||||
};
|
||||
});
|
||||
if (selectedChoice) {
|
||||
let requestData = null;
|
||||
if (selectedChoice.action === choices[0]) {
|
||||
requestData = motions.map(motion => {
|
||||
let tagIds = [...motion.tags_id, ...(selectedChoice.items as number[])];
|
||||
tagIds = tagIds.filter((id, index, self) => self.indexOf(id) === index); // remove duplicates
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: tagIds
|
||||
};
|
||||
});
|
||||
// await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||
} else if (selectedChoice.action === choices[1]) {
|
||||
requestData = motions.map(motion => {
|
||||
const tagIdsToRemove = selectedChoice.items as number[];
|
||||
const tagIds = motion.tags_id.filter(id => !tagIdsToRemove.includes(id));
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: tagIds
|
||||
};
|
||||
});
|
||||
// await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||
} else if (selectedChoice.action === choices[2]) {
|
||||
requestData = motions.map(motion => {
|
||||
return {
|
||||
id: motion.id,
|
||||
tags: []
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const message = `${motions.length} ` + this.translate.instant(this.messageForSpinner);
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
await this.httpService.post('/rest/motions/motion/manage_multiple_tags/', { motions: requestData });
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -262,10 +315,19 @@ export class MotionMultiselectService {
|
||||
clearChoice
|
||||
);
|
||||
if (selectedChoice) {
|
||||
let i = 0;
|
||||
for (const motion of motions) {
|
||||
++i;
|
||||
const message =
|
||||
this.translate.instant(this.messageForSpinner) +
|
||||
`\n${i} ` +
|
||||
this.translate.instant('of') +
|
||||
` ${motions.length}`;
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
const blockId = selectedChoice.action ? null : (selectedChoice.items as number);
|
||||
await this.repo.update({ motion_block_id: blockId }, motion);
|
||||
}
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -317,8 +379,11 @@ export class MotionMultiselectService {
|
||||
];
|
||||
const selectedChoice = await this.choiceService.open(title, choices);
|
||||
if (selectedChoice && motions.length) {
|
||||
const message = this.translate.instant(`I have ${motions.length} favorite motions. Please wait...`);
|
||||
const star = (selectedChoice.items as number) === choices[0].id;
|
||||
this.spinnerService.setVisibility(true, message);
|
||||
await this.personalNoteService.bulkSetStar(motions, star);
|
||||
this.spinnerService.setVisibility(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
<div class="nav-toolbar">
|
||||
<!-- logo -->
|
||||
<a routerLink="/" (click)="toggleSideNav()">
|
||||
<os-logo class="os-logo-container" inputAction="logo_web_header" [footer]="false"></os-logo>
|
||||
<os-logo class="os-logo-container" [footer]="false"></os-logo>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -130,7 +130,9 @@
|
||||
<small><os-copyright-sign></os-copyright-sign> Copyright by OpenSlides</small>
|
||||
</span>
|
||||
</a>
|
||||
<div class="os-footer-logo-container"><os-logo inputAction="logo_web_header" footer="true"> </os-logo></div>
|
||||
<div class="os-footer-logo-container">
|
||||
<os-logo [footer]="true"></os-logo>
|
||||
</div>
|
||||
</mat-nav-list>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
|
75
client/src/assets/styles/openslides-developer-theme.scss
Normal file
75
client/src/assets/styles/openslides-developer-theme.scss
Normal file
@ -0,0 +1,75 @@
|
||||
$openslides-developer-primary: (
|
||||
50 : #000000,
|
||||
100 : #000000,
|
||||
200 : #000000,
|
||||
300 : #000000,
|
||||
400 : #000000,
|
||||
500 : #000000,
|
||||
600 : #000000,
|
||||
700 : #000000,
|
||||
800 : #000000,
|
||||
900 : #000000,
|
||||
A100 : #000000,
|
||||
A200 : #000000,
|
||||
A400 : #000000,
|
||||
A700 : #000000,
|
||||
contrast: (
|
||||
50 : #000000,
|
||||
100 : #000000,
|
||||
200 : #000000,
|
||||
300 : #000000,
|
||||
400 : #000000,
|
||||
500 : #000000,
|
||||
600 : #000000,
|
||||
700 : #000000,
|
||||
800 : #000000,
|
||||
900 : #000000,
|
||||
A100 : #000000,
|
||||
A200 : #000000,
|
||||
A400 : #000000,
|
||||
A700 : #000000,
|
||||
)
|
||||
);
|
||||
|
||||
$openslides-developer-accent: (
|
||||
50 : #f4f9e1,
|
||||
100 : #e5f0b3,
|
||||
200 : #d3e781,
|
||||
300 : #c1dd4e,
|
||||
400 : #b4d528,
|
||||
500 : #a7ce02,
|
||||
600 : #9fc902,
|
||||
700 : #96c201,
|
||||
800 : #8cbc01,
|
||||
900 : #7cb001,
|
||||
A100 : #f2ffda,
|
||||
A200 : #e1ffa7,
|
||||
A400 : #cfff74,
|
||||
A700 : #c6ff5a,
|
||||
contrast: (
|
||||
50 : #000000,
|
||||
100 : #000000,
|
||||
200 : #000000,
|
||||
300 : #000000,
|
||||
400 : #000000,
|
||||
500 : #000000,
|
||||
600 : #000000,
|
||||
700 : #000000,
|
||||
800 : #000000,
|
||||
900 : #000000,
|
||||
A100 : #000000,
|
||||
A200 : #000000,
|
||||
A400 : #000000,
|
||||
A700 : #000000,
|
||||
)
|
||||
);
|
||||
|
||||
$openslides-primary: mat-palette($openslides-developer-primary);
|
||||
$openslides-accent: mat-palette($openslides-developer-accent);
|
||||
$openslides-warn: mat-palette($mat-pink);
|
||||
|
||||
$openslides-developer-theme: mat-dark-theme(
|
||||
$openslides-primary,
|
||||
$openslides-accent,
|
||||
$openslides-warn
|
||||
);
|
40
client/src/assets/styles/openslides-general-theme.scss
Normal file
40
client/src/assets/styles/openslides-general-theme.scss
Normal file
@ -0,0 +1,40 @@
|
||||
$openslides-primary-dark: (
|
||||
50 : #e0e0e0,
|
||||
100 : #b3b3b3,
|
||||
200 : #808080,
|
||||
300 : #4d4d4d,
|
||||
400 : #262626,
|
||||
500 : #000000,
|
||||
600 : #000000,
|
||||
700 : #000000,
|
||||
800 : #000000,
|
||||
900 : #000000,
|
||||
A100 : #a6a6a6,
|
||||
A200 : #8c8c8c,
|
||||
A400 : #737373,
|
||||
A700 : #666666,
|
||||
contrast: (
|
||||
50 : #000000,
|
||||
100 : #000000,
|
||||
200 : #000000,
|
||||
300 : #ffffff,
|
||||
400 : #ffffff,
|
||||
500 : #ffffff,
|
||||
600 : #ffffff,
|
||||
700 : #ffffff,
|
||||
800 : #ffffff,
|
||||
900 : #ffffff,
|
||||
A100 : #000000,
|
||||
A200 : #000000,
|
||||
A400 : #ffffff,
|
||||
A700 : #ffffff,
|
||||
)
|
||||
);
|
||||
|
||||
$openslides-primary: mat-palette($openslides-primary-dark, 500, 700, 900);
|
||||
|
||||
$openslides-general-theme: mat-light-theme(
|
||||
$openslides-primary,
|
||||
$openslides-primary,
|
||||
$openslides-primary
|
||||
);
|
@ -13,7 +13,7 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
</head>
|
||||
|
||||
<body class="openslides-theme">
|
||||
<body class="general-theme">
|
||||
<os-root></os-root>
|
||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||
</body>
|
||||
|
@ -3,9 +3,11 @@
|
||||
@include mat-core();
|
||||
|
||||
/** Import brand theme */
|
||||
@import './assets/styles/openslides-general-theme.scss';
|
||||
@import './assets/styles/openslides-theme.scss';
|
||||
@import './assets/styles/openslides-dark-theme.scss';
|
||||
@import './assets/styles/openslides-green-theme.scss';
|
||||
@import './assets/styles/openslides-developer-theme.scss';
|
||||
|
||||
/** Import the component-related style sheets here */
|
||||
@import './app/site/site.component.scss-theme.scss';
|
||||
@ -13,6 +15,7 @@
|
||||
@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/shared/components/sorting-tree/sorting-tree.component.scss';
|
||||
@import './app/site/global-spinner/global-spinner.component.scss';
|
||||
|
||||
/** fonts */
|
||||
@import './assets/styles/fonts.scss';
|
||||
@ -25,6 +28,7 @@
|
||||
@include os-projector-button-style($theme);
|
||||
@include os-list-of-speakers-style($theme);
|
||||
@include os-sorting-tree-style($theme);
|
||||
@include os-global-spinner-theme($theme);
|
||||
/** More components are added here */
|
||||
}
|
||||
|
||||
@ -34,10 +38,34 @@
|
||||
/** Load projector specific SCSS values */
|
||||
@import './assets/styles/projector.scss';
|
||||
|
||||
.general-theme {
|
||||
@include os-global-spinner-theme($openslides-general-theme);
|
||||
}
|
||||
|
||||
/** Define the classes to switch between themes */
|
||||
.openslides-theme {
|
||||
@include angular-material-theme($openslides-theme);
|
||||
@include openslides-components-theme($openslides-theme);
|
||||
|
||||
.logo-container {
|
||||
img.dark {
|
||||
display: none;
|
||||
}
|
||||
img.default {
|
||||
display: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.openslides-dark-theme, .openslides-developer-dark-theme {
|
||||
.logo-container {
|
||||
img.dark {
|
||||
display: inherit;
|
||||
}
|
||||
img.default {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.openslides-dark-theme {
|
||||
@ -50,17 +78,71 @@
|
||||
@include openslides-components-theme($openslides-green-theme);
|
||||
}
|
||||
|
||||
.openslides-developer-dark-theme {
|
||||
@include angular-material-theme($openslides-developer-theme);
|
||||
@include openslides-components-theme($openslides-developer-theme);
|
||||
|
||||
* {
|
||||
font-family: monospace !important;
|
||||
color: mat-color(map-get($openslides-developer-theme, accent));
|
||||
}
|
||||
|
||||
os-site {
|
||||
.active mat-icon, .active span {
|
||||
color: mat-color(map-get($openslides-developer-theme, accent));
|
||||
}
|
||||
}
|
||||
|
||||
.custom-table-header {
|
||||
background: #000;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.mat-toolbar.sticky-toolbar {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.mat-drawer-inner-container {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.mat-drawer-inner-container > * {
|
||||
background: #000 !important;
|
||||
z-index: 3;
|
||||
|
||||
.os-site.nav-toolbar {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
.mat-drawer-content > * {
|
||||
background: #000;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-family: 'Material Icons' !important;
|
||||
}
|
||||
|
||||
.mat-mini-fab.projector-active {
|
||||
background-color: mat-color(map-get($openslides-developer-theme, warn));
|
||||
}
|
||||
|
||||
.mce-ico {
|
||||
font-family: 'tinymce', Arial !important;
|
||||
}
|
||||
}
|
||||
|
||||
/** Define the general style-rules */
|
||||
* {
|
||||
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.mat-toolbar h2,
|
||||
.mat-dialog-title {
|
||||
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif !important;
|
||||
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-family: 'Material Icons Baseline';
|
||||
font-family: 'Material Icons' !important;
|
||||
}
|
||||
|
||||
body {
|
||||
|
@ -121,6 +121,10 @@ def get_config_variables():
|
||||
{"value": "openslides-theme", "display_name": "OpenSlides Default"},
|
||||
{"value": "openslides-dark-theme", "display_name": "OpenSlides Dark"},
|
||||
{"value": "openslides-green-theme", "display_name": "OpenSlides Green"},
|
||||
{
|
||||
"value": "openslides-developer-dark-theme",
|
||||
"display_name": "OpenSlides Developer",
|
||||
},
|
||||
),
|
||||
weight=141,
|
||||
group="General",
|
||||
|
@ -562,6 +562,7 @@ class UserLoginView(WhoAmIDataView):
|
||||
context["legal_notice"] = config["general_event_legal_notice"]
|
||||
# Add the theme, so the loginpage is themed correctly
|
||||
context["theme"] = config["openslides_theme"]
|
||||
context["logo_web_header"] = config["logo_web_header"]
|
||||
else:
|
||||
# self.request.method == 'POST'
|
||||
context.update(self.get_whoami_data())
|
||||
|
Loading…
Reference in New Issue
Block a user