Merge pull request #4215 from GabrielInTheWorld/theming
OpenSlides Theming
This commit is contained in:
commit
9c6a21469b
@ -5,6 +5,7 @@ import { LoginDataService } from './core/services/login-data.service';
|
|||||||
import { ConfigService } from './core/services/config.service';
|
import { ConfigService } from './core/services/config.service';
|
||||||
import { ConstantsService } from './core/services/constants.service';
|
import { ConstantsService } from './core/services/constants.service';
|
||||||
import { ServertimeService } from './core/services/servertime.service';
|
import { ServertimeService } from './core/services/servertime.service';
|
||||||
|
import { ThemeService } from './core/services/theme.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Angular's global App Component
|
* Angular's global App Component
|
||||||
@ -25,6 +26,7 @@ export class AppComponent {
|
|||||||
* @param autoupdateService
|
* @param autoupdateService
|
||||||
* @param notifyService
|
* @param notifyService
|
||||||
* @param translate
|
* @param translate
|
||||||
|
* @param themeService used to listen to theme-changes
|
||||||
*/
|
*/
|
||||||
public constructor(
|
public constructor(
|
||||||
translate: TranslateService,
|
translate: TranslateService,
|
||||||
@ -32,7 +34,8 @@ export class AppComponent {
|
|||||||
configService: ConfigService,
|
configService: ConfigService,
|
||||||
loginDataService: LoginDataService,
|
loginDataService: LoginDataService,
|
||||||
constantsService: ConstantsService, // Needs to be started, so it can register itself to the WebsocketService
|
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
|
// manually add the supported languages
|
||||||
translate.addLangs(['en', 'de', 'cs']);
|
translate.addLangs(['en', 'de', 'cs']);
|
||||||
|
12
client/src/app/core/services/theme.service.spec.ts
Normal file
12
client/src/app/core/services/theme.service.spec.ts
Normal file
@ -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();
|
||||||
|
});
|
||||||
|
});
|
31
client/src/app/core/services/theme.service.ts
Normal file
31
client/src/app/core/services/theme.service.ts
Normal file
@ -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.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import { Component, OnInit, Input } from '@angular/core';
|
import { Component, OnInit, Input } from '@angular/core';
|
||||||
|
|
||||||
import { MediaManageService } from '../../../site/mediafiles/services/media-manage.service';
|
import { MediaManageService } from '../../../site/mediafiles/services/media-manage.service';
|
||||||
|
import { ConfigService } from 'app/core/services/config.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reusable Logo component for Apps.
|
* Reusable Logo component for Apps.
|
||||||
@ -38,10 +39,15 @@ import { MediaManageService } from '../../../site/mediafiles/services/media-mana
|
|||||||
})
|
})
|
||||||
export class LogoComponent implements OnInit {
|
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';
|
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
|
* Holds the actions for logos. Updated via an observable
|
||||||
*/
|
*/
|
||||||
@ -66,11 +72,12 @@ export class LogoComponent implements OnInit {
|
|||||||
@Input()
|
@Input()
|
||||||
public alignment = 'center';
|
public alignment = 'center';
|
||||||
/**
|
/**
|
||||||
* The consotructor
|
* The constructor
|
||||||
*
|
*
|
||||||
* @param mmservice The Media Manage Service
|
* @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
|
* Initialization function
|
||||||
@ -97,6 +104,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<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
|
* gets the header image based on logo action
|
||||||
*
|
*
|
||||||
@ -120,7 +142,7 @@ export class LogoComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (path === '') {
|
if (path === '') {
|
||||||
path = LogoComponent.STANDARD_LOGO;
|
path = this.getImagePathRelatedToTheme();
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
@ -147,10 +169,14 @@ export class LogoComponent implements OnInit {
|
|||||||
* logo was set
|
* logo was set
|
||||||
*/
|
*/
|
||||||
protected getFooterImage(logoAction: string): string {
|
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 '';
|
return '';
|
||||||
} else {
|
} else {
|
||||||
return LogoComponent.STANDARD_LOGO;
|
return this.getImagePathRelatedToTheme();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
os-site {
|
os-site {
|
||||||
/* main background color */
|
/* main background color */
|
||||||
.main-container {
|
.main-container {
|
||||||
background-color: $os-background;
|
background-color: mat-color($background, background);
|
||||||
}
|
}
|
||||||
.nav-toolbar {
|
.nav-toolbar {
|
||||||
background-color: mat-color($background, card); //TODO
|
background-color: mat-color($background, card); //TODO
|
||||||
@ -37,10 +37,10 @@
|
|||||||
/** style the active link */
|
/** style the active link */
|
||||||
.active {
|
.active {
|
||||||
mat-icon {
|
mat-icon {
|
||||||
color: $os-primary;
|
color: mat-color($primary);
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
color: $os-primary;
|
color: mat-color($primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
34
client/src/assets/styles/global-components-style.scss
Normal file
34
client/src/assets/styles/global-components-style.scss
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
9
client/src/assets/styles/openslides-dark-theme.scss
Normal file
9
client/src/assets/styles/openslides-dark-theme.scss
Normal file
@ -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
|
||||||
|
);
|
42
client/src/assets/styles/openslides-green-theme.scss
Normal file
42
client/src/assets/styles/openslides-green-theme.scss
Normal file
@ -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
|
||||||
|
)
|
@ -79,9 +79,22 @@ $openslides-warn: mat-palette($mat-red);
|
|||||||
$openslides-theme: mat-light-theme($openslides-primary, $openslides-accent, $openslides-warn);
|
$openslides-theme: mat-light-theme($openslides-primary, $openslides-accent, $openslides-warn);
|
||||||
|
|
||||||
// Create Sass color vars (for using in scss files).
|
// Create Sass color vars (for using in scss files).
|
||||||
$os-primary: mat-color($openslides-primary);
|
// This will be set dynamically with the selecting theme
|
||||||
$os-accent: mat-color($openslides-accent);
|
// $os-primary: mat-color($openslides-primary);
|
||||||
$os-warn: mat-color($openslides-warn);
|
// $os-accent: mat-color($openslides-accent);
|
||||||
|
// $os-warn: mat-color($openslides-warn);
|
||||||
|
|
||||||
$os-outline: mat-color($mat-grey, 300);
|
$os-outline: mat-color($mat-grey, 300);
|
||||||
$os-background: mat-color($mat-grey, 100);
|
$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));
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
<meta name="theme-color" content="#317796">
|
<meta name="theme-color" content="#317796">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body class="openslides-theme">
|
||||||
<os-root></os-root>
|
<os-root></os-root>
|
||||||
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
<noscript>Please enable JavaScript to continue using this application.</noscript>
|
||||||
</body>
|
</body>
|
||||||
|
@ -1,23 +1,45 @@
|
|||||||
|
/** Imports the material-design-theming */
|
||||||
@import '~@angular/material/theming';
|
@import '~@angular/material/theming';
|
||||||
@include mat-core();
|
@include mat-core();
|
||||||
|
|
||||||
/** Import brand theme and (new) component themes */
|
/** Import brand theme */
|
||||||
@import './assets/styles/openslides-theme.scss';
|
@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 */
|
/** fonts */
|
||||||
@import './assets/styles/fonts.scss';
|
@import './assets/styles/fonts.scss';
|
||||||
|
|
||||||
|
/** Mix the component-related style-rules */
|
||||||
@mixin openslides-components-theme($theme) {
|
@mixin openslides-components-theme($theme) {
|
||||||
@include os-site-theme($theme);
|
@include os-site-theme($theme);
|
||||||
|
@include os-components-style($theme);
|
||||||
/** More components are added here */
|
/** 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';
|
@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;
|
font-family: OSFont, Fira Sans, Roboto, Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
@ -30,12 +52,10 @@ mat-icon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
// background: #e8eaed;
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #222;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
@ -50,7 +70,6 @@ h3,
|
|||||||
h1 {
|
h1 {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: $os-primary;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
font-size: 36px;
|
font-size: 36px;
|
||||||
@ -59,13 +78,10 @@ h3 {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
h3.accent {
|
|
||||||
color: $os-accent;
|
|
||||||
}
|
|
||||||
h4 {
|
h4 {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: rgba(0, 0, 0, 0.54);
|
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
|
|
||||||
.mat-icon-button mat-icon {
|
.mat-icon-button mat-icon {
|
||||||
@ -83,7 +99,6 @@ img {
|
|||||||
|
|
||||||
a {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #039be5; /*TODO: move to theme*/
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
@ -121,9 +136,6 @@ b {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.accent-text {
|
|
||||||
color: mat-color($openslides-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.green-text {
|
.green-text {
|
||||||
// TODO better name/theming
|
// TODO better name/theming
|
||||||
@ -209,7 +221,6 @@ mat-card {
|
|||||||
height: 60px;
|
height: 60px;
|
||||||
line-height: 60px;
|
line-height: 60px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
background: white;
|
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@ -228,6 +239,7 @@ mat-card {
|
|||||||
|
|
||||||
mat-icon {
|
mat-icon {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
|
margin-right: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,7 +398,6 @@ button.mat-menu-item.selected {
|
|||||||
}
|
}
|
||||||
.button24 {
|
.button24 {
|
||||||
background-color: white;
|
background-color: white;
|
||||||
color: $os-primary !important;
|
|
||||||
width: 24px !important;
|
width: 24px !important;
|
||||||
height: 24px !important;
|
height: 24px !important;
|
||||||
}
|
}
|
||||||
|
@ -112,6 +112,21 @@ def get_config_variables():
|
|||||||
subgroup="System",
|
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
|
# General export settings
|
||||||
|
|
||||||
yield ConfigVariable(
|
yield ConfigVariable(
|
||||||
|
Loading…
Reference in New Issue
Block a user