diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index 17b4e0c4c..f4adef4ac 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -5,6 +5,7 @@ import { LoginDataService } from './core/services/login-data.service'; import { ConfigService } from './core/services/config.service'; import { ConstantsService } from './core/services/constants.service'; import { ServertimeService } from './core/services/servertime.service'; +import { ThemeService } from './core/services/theme.service'; /** * Angular's global App Component @@ -25,6 +26,7 @@ export class AppComponent { * @param autoupdateService * @param notifyService * @param translate + * @param themeService used to listen to theme-changes */ public constructor( translate: TranslateService, @@ -32,7 +34,8 @@ export class AppComponent { configService: ConfigService, loginDataService: LoginDataService, constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService - servertimeService: ServertimeService + servertimeService: ServertimeService, + themeService: ThemeService ) { // manually add the supported languages translate.addLangs(['en', 'de', 'cs']); diff --git a/client/src/app/core/services/theme.service.spec.ts b/client/src/app/core/services/theme.service.spec.ts new file mode 100644 index 000000000..51fe21708 --- /dev/null +++ b/client/src/app/core/services/theme.service.spec.ts @@ -0,0 +1,12 @@ +import { TestBed } from '@angular/core/testing'; + +import { ThemeService } from './theme.service'; + +describe('ThemeService', () => { + beforeEach(() => TestBed.configureTestingModule({})); + + it('should be created', () => { + const service: ThemeService = TestBed.get(ThemeService); + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/core/services/theme.service.ts b/client/src/app/core/services/theme.service.ts new file mode 100644 index 000000000..c7e507996 --- /dev/null +++ b/client/src/app/core/services/theme.service.ts @@ -0,0 +1,31 @@ +import { Injectable } from '@angular/core'; +import { ConfigService } from './config.service'; + +/** + * Service to set the theme for the OpenSlides. + * Reads related data from server, + * the server sends the value of the new theme --> the new theme has only to be added to the body. + */ +@Injectable({ + providedIn: 'root' +}) +export class ThemeService { + /** + * Here it will subscribe to the observer from config-service to read data. + * + * @param configService must be injected to get the data from server. + */ + public constructor(configService: ConfigService) { + configService.get('openslides_theme').subscribe(newTheme => { + // Listen to the related event. + const classList = document.getElementsByTagName('body')[0].classList; // Get the classlist of the body. + if (newTheme) { + 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. + } + }); + } +} diff --git a/client/src/app/shared/components/logo/logo.component.ts b/client/src/app/shared/components/logo/logo.component.ts index 92285586d..77473f0b6 100644 --- a/client/src/app/shared/components/logo/logo.component.ts +++ b/client/src/app/shared/components/logo/logo.component.ts @@ -1,5 +1,6 @@ import { Component, OnInit, Input } from '@angular/core'; import { MediaManageService } from '../../../site/mediafiles/services/media-manage.service'; +import { ConfigService } from 'app/core/services/config.service'; /** * Reusable Logo component for Apps. @@ -37,10 +38,15 @@ import { MediaManageService } from '../../../site/mediafiles/services/media-mana }) export class LogoComponent implements OnInit { /** - * Constant path of the dark logo + * 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 actions for logos. Updated via an observable */ @@ -65,11 +71,12 @@ export class LogoComponent implements OnInit { @Input() public alignment = 'center'; /** - * The consotructor + * The constructor * * @param mmservice The Media Manage Service + * @param configService The ConfigService to subscribe to theme-changes */ - public constructor(private mmservice: MediaManageService) {} + public constructor(private mmservice: MediaManageService, private configService: ConfigService) {} /** * Initialization function @@ -96,6 +103,21 @@ export class LogoComponent implements OnInit { } } + /** + * 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('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 * @@ -119,7 +141,7 @@ export class LogoComponent implements OnInit { } } if (path === '') { - path = LogoComponent.STANDARD_LOGO; + path = this.getImagePathRelatedToTheme(); } return path; } @@ -146,10 +168,14 @@ export class LogoComponent implements OnInit { * logo was set */ protected getFooterImage(logoAction: string): string { - if (this.getHeaderImage(logoAction) === LogoComponent.STANDARD_LOGO || this.getHeaderImage(logoAction) === '') { + if ( + this.getHeaderImage(logoAction) === LogoComponent.STANDARD_LOGO || + this.getHeaderImage(logoAction) === LogoComponent.STANDARD_LOGO_DARK_THEME || + this.getHeaderImage(logoAction) === '' + ) { return ''; } else { - return LogoComponent.STANDARD_LOGO; + return this.getImagePathRelatedToTheme(); } } } diff --git a/client/src/app/site/site.component.scss-theme.scss b/client/src/app/site/site.component.scss-theme.scss index 3d79bc4f9..9140e26f4 100644 --- a/client/src/app/site/site.component.scss-theme.scss +++ b/client/src/app/site/site.component.scss-theme.scss @@ -11,7 +11,7 @@ os-site { /* main background color */ .main-container { - background-color: $os-background; + background-color: mat-color($background, background); } .nav-toolbar { background-color: mat-color($background, card); //TODO @@ -37,10 +37,10 @@ /** style the active link */ .active { mat-icon { - color: $os-primary; + color: mat-color($primary); } span { - color: $os-primary; + color: mat-color($primary); } } } diff --git a/client/src/assets/styles/global-components-style.scss b/client/src/assets/styles/global-components-style.scss new file mode 100644 index 000000000..dd0245fa3 --- /dev/null +++ b/client/src/assets/styles/global-components-style.scss @@ -0,0 +1,34 @@ +// Material theming import +@import '~@angular/material/theming'; + +/** + * Mixin-style to style global classes and tags with theme-related colors. + */ +@mixin os-components-style($theme) { + $primary: map-get($theme, primary); + $accent: map-get($theme, accent); + $warn: map-get($theme, warn); + $foreground: map-get($theme, foreground); + $background: map-get($theme, background); + + h1, h3.accent { + color: mat-color($primary); + } + + a { + color: mat-color($primary); + } + + .accent-text { + color: mat-color($accent); + } + + .button24 { + color: mat-color($primary); + } + + //custom table header for search button, filtering and more. Used in ListViews + .custom-table-header { + background: mat-color($background, background); + } +} diff --git a/client/src/assets/styles/openslides-dark-theme.scss b/client/src/assets/styles/openslides-dark-theme.scss new file mode 100644 index 000000000..2cdf28e8e --- /dev/null +++ b/client/src/assets/styles/openslides-dark-theme.scss @@ -0,0 +1,9 @@ +$openslides-primary: mat-palette($mat-light-blue, 500, 100, 700); +$openslides-accent: mat-palette($mat-orange, 700, 300, 900); +$openslides-warn: mat-palette($mat-red, 600, 500, 700); + +$openslides-dark-theme: mat-dark-theme( + $openslides-primary, + $openslides-accent, + $openslides-warn +); diff --git a/client/src/assets/styles/openslides-green-theme.scss b/client/src/assets/styles/openslides-green-theme.scss new file mode 100644 index 000000000..456d9c13e --- /dev/null +++ b/client/src/assets/styles/openslides-green-theme.scss @@ -0,0 +1,42 @@ +$openslides-green: ( + 50: #e9f2e6, + 100: #c8e0bf, + 200: #a3cb95, + 300: #7eb66b, + 400: #62a64b, + 500: #46962b, + 600: #3f8e26, + 700: #0a321e, + 800: #092d1a, + 900: #072616, + A100: #acff9d, + A200: #80ff6a, + A400: #55ff37, + A700: #3fff1e, + contrast: ( + 50: #000000, + 100: #000000, + 200: #000000, + 300: #000000, + 400: #000000, + 500: #ffffff, + 600: #ffffff, + 700: #ffffff, + 800: #ffffff, + 900: #ffffff, + A100: #000000, + A200: #000000, + A400: #000000, + A700: #000000 + ) +); + +$openslides-primary: mat-palette($openslides-green); +$openslides-accent: mat-palette($mat-amber); +$openslides-warn: mat-palette($mat-red); + +$openslides-green-theme: mat-light-theme( + $openslides-primary, + $openslides-accent, + $openslides-warn +) diff --git a/client/src/assets/styles/openslides-theme.scss b/client/src/assets/styles/openslides-theme.scss index f7af5bf86..2952fda99 100644 --- a/client/src/assets/styles/openslides-theme.scss +++ b/client/src/assets/styles/openslides-theme.scss @@ -79,9 +79,22 @@ $openslides-warn: mat-palette($mat-red); $openslides-theme: mat-light-theme($openslides-primary, $openslides-accent, $openslides-warn); // Create Sass color vars (for using in scss files). -$os-primary: mat-color($openslides-primary); -$os-accent: mat-color($openslides-accent); -$os-warn: mat-color($openslides-warn); +// This will be set dynamically with the selecting theme +// $os-primary: mat-color($openslides-primary); +// $os-accent: mat-color($openslides-accent); +// $os-warn: mat-color($openslides-warn); $os-outline: mat-color($mat-grey, 300); $os-background: mat-color($mat-grey, 100); + +/** This is the workaround to set a custom background-color + * In the first step the color must be merged, in order to have to a map. + * The components will get a value from this map. + */ +$background: map-get($openslides-theme, background); +$background: map_merge($background, (background: $os-background)); + +/** + * Merge the theme with the custom-background. + */ +$openslides-theme: map_merge($openslides-theme, (background: $background)); diff --git a/client/src/index.html b/client/src/index.html index 86e4969d0..e95a941de 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -12,7 +12,7 @@ - + diff --git a/client/src/styles.scss b/client/src/styles.scss index 2ddc0baf0..e35b5a9a3 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -1,23 +1,45 @@ +/** Imports the material-design-theming */ @import '~@angular/material/theming'; @include mat-core(); -/** Import brand theme and (new) component themes */ +/** Import brand theme */ @import './assets/styles/openslides-theme.scss'; -@import './app/site/site.component.scss-theme'; +@import './assets/styles/openslides-dark-theme.scss'; +@import './assets/styles/openslides-green-theme.scss'; + +/** Import the component-related stylesheets here */ +@import './app/site/site.component.scss-theme.scss'; +@import './assets/styles/global-components-style.scss'; /** fonts */ @import './assets/styles/fonts.scss'; +/** Mix the component-related style-rules */ @mixin openslides-components-theme($theme) { @include os-site-theme($theme); + @include os-components-style($theme); /** More components are added here */ } -@include angular-material-theme($openslides-theme); -@include openslides-components-theme($openslides-theme); - @import '~angular-tree-component/dist/angular-tree-component.css'; +/** Define the classes to switch between themes */ +.openslides-theme { + @include angular-material-theme($openslides-theme); + @include openslides-components-theme($openslides-theme); +} + +.openslides-dark-theme { + @include angular-material-theme($openslides-dark-theme); + @include openslides-components-theme($openslides-dark-theme); +} + +.openslides-green-theme { + @include angular-material-theme($openslides-green-theme); + @include openslides-components-theme($openslides-green-theme); +} + +/** Define the general style-rules */ * { font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif; } @@ -30,12 +52,10 @@ mat-icon { } body { - // background: #e8eaed; margin: 0 auto; padding: 0; line-height: 1.5; font-size: 14px; - color: #222; } h1, @@ -50,7 +70,6 @@ h3, h1 { padding-bottom: 10px; line-height: 1.2; - color: $os-primary; margin: 0; font-weight: normal; font-size: 36px; @@ -59,13 +78,10 @@ h3 { font-weight: 500; margin-bottom: 0; } -h3.accent { - color: $os-accent; -} + h4 { font-weight: 400; font-size: 12px; - color: rgba(0, 0, 0, 0.54); margin-bottom: 5px; .mat-icon-button mat-icon { @@ -83,7 +99,6 @@ img { a { text-decoration: none; - color: #039be5; /*TODO: move to theme*/ &:hover { text-decoration: underline; @@ -121,9 +136,6 @@ b { } } -.accent-text { - color: mat-color($openslides-accent); -} .green-text { // TODO better name/theming @@ -209,7 +221,6 @@ mat-card { height: 60px; line-height: 60px; text-align: right; - background: white; border-bottom: 1px solid rgba(0, 0, 0, 0.12); display: flex; justify-content: flex-end; @@ -228,6 +239,7 @@ mat-card { mat-icon { vertical-align: text-bottom; + margin-right: 2px; } } @@ -386,7 +398,6 @@ button.mat-menu-item.selected { } .button24 { background-color: white; - color: $os-primary !important; width: 24px !important; height: 24px !important; } diff --git a/openslides/core/config_variables.py b/openslides/core/config_variables.py index 42abf8a5b..a99143aa7 100644 --- a/openslides/core/config_variables.py +++ b/openslides/core/config_variables.py @@ -112,6 +112,21 @@ def get_config_variables(): subgroup="System", ) + yield ConfigVariable( + name="openslides_theme", + default_value="openslides-theme", + input_type="choice", + label="OpenSlides Theme", + choices=( + {"value": "openslides-theme", "display_name": "OpenSlides Default"}, + {"value": "openslides-dark-theme", "display_name": "OpenSlides Dark"}, + {"value": "openslides-green-theme", "display_name": "OpenSlides Green"}, + ), + weight=141, + group="General", + subgroup="System", + ) + # General export settings yield ConfigVariable(