From b3cf312edcefec3fb26bad8637f3a0969504be87 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 29 Mar 2022 22:18:40 +0200 Subject: [PATCH] Start theming providers Signed-off-by: Joas Schilling --- apps/theming/appinfo/routes.php | 4 +- apps/theming/css/settings-admin.scss | 2 + apps/theming/js/settings-admin.js | 52 +++--- .../lib/Controller/ThemingController.php | 109 +++++------- apps/theming/lib/ITheme.php | 55 ++++++ .../BeforeTemplateRenderedListener.php | 49 +++--- .../lib/Service/ThemeInjectionService.php | 88 ++++++++++ apps/theming/lib/Service/ThemesService.php | 56 ++++++ .../lib/Themes/DarkHighContrastTheme.php | 47 +++++ apps/theming/lib/Themes/DarkTheme.php | 75 ++++++++ apps/theming/lib/Themes/DefaultTheme.php | 160 ++++++++++++++++++ apps/theming/lib/Themes/HighContrastTheme.php | 47 +++++ apps/theming/lib/Util.php | 75 +++----- composer.json | 3 +- composer.lock | 58 ++++++- core/css/apps.scss | 2 + core/css/header.scss | 5 + core/css/styles.scss | 1 + core/templates/layout.user.php | 5 +- lib/private/TemplateLayout.php | 11 -- lib/private/legacy/OC_Template.php | 2 +- 21 files changed, 712 insertions(+), 194 deletions(-) create mode 100644 apps/theming/lib/ITheme.php create mode 100644 apps/theming/lib/Service/ThemeInjectionService.php create mode 100644 apps/theming/lib/Service/ThemesService.php create mode 100644 apps/theming/lib/Themes/DarkHighContrastTheme.php create mode 100644 apps/theming/lib/Themes/DarkTheme.php create mode 100644 apps/theming/lib/Themes/DefaultTheme.php create mode 100644 apps/theming/lib/Themes/HighContrastTheme.php diff --git a/apps/theming/appinfo/routes.php b/apps/theming/appinfo/routes.php index 0628ade8032..358f6a39ad4 100644 --- a/apps/theming/appinfo/routes.php +++ b/apps/theming/appinfo/routes.php @@ -44,8 +44,8 @@ return ['routes' => [ 'verb' => 'POST' ], [ - 'name' => 'Theming#getStylesheet', - 'url' => '/styles', + 'name' => 'Theming#getThemeVariables', + 'url' => '/theme/{themeId}.css', 'verb' => 'GET', ], [ diff --git a/apps/theming/css/settings-admin.scss b/apps/theming/css/settings-admin.scss index 504760d4596..c4d67917506 100644 --- a/apps/theming/css/settings-admin.scss +++ b/apps/theming/css/settings-admin.scss @@ -100,6 +100,7 @@ margin-top: 10px; margin-bottom: 20px; cursor: pointer; + background-image: var(--image-login); #theming-preview-logo { cursor: pointer; @@ -110,6 +111,7 @@ background-position: center; background-repeat: no-repeat; background-size: contain; + background-image: var(--image-logo); } } diff --git a/apps/theming/js/settings-admin.js b/apps/theming/js/settings-admin.js index 335492fdae2..7efdab6dda4 100644 --- a/apps/theming/js/settings-admin.js +++ b/apps/theming/js/settings-admin.js @@ -28,9 +28,9 @@ function setThemingValue(setting, value) { startLoading(); $.post( OC.generateUrl('/apps/theming/ajax/updateStylesheet'), {'setting' : setting, 'value' : value} - ).done(function(response) { + ).done(function() { hideUndoButton(setting, value); - preview(setting, value, response.data.serverCssUrl); + preview(setting, value); }).fail(function(response) { OC.msg.finishedSaving('#theming_settings_msg', response.responseJSON); $('#theming_settings_loading').hide(); @@ -39,41 +39,31 @@ function setThemingValue(setting, value) { function preview(setting, value, serverCssUrl) { OC.msg.startAction('#theming_settings_msg', t('theming', 'Loading preview…')); - var stylesheetsLoaded = 1; - var reloadStylesheets = function(cssFile) { - var queryString = '?reload=' + new Date().getTime(); - var url = cssFile + queryString; - var old = $('link[href*="' + cssFile + '"]'); - var stylesheet = $("", { - rel: "stylesheet", - type: "text/css", - href: url - }); - stylesheet.load(function () { - $(old).remove(); - stylesheetsLoaded--; - if(stylesheetsLoaded === 0) { - $('#theming_settings_loading').hide(); - var response = { status: 'success', data: {message: t('theming', 'Saved')}}; - OC.msg.finishedSaving('#theming_settings_msg', response); - } - }); - stylesheet.appendTo("head"); - }; - - if (serverCssUrl !== undefined) { - stylesheetsLoaded++; - reloadStylesheets(serverCssUrl); - } - reloadStylesheets(OC.generateUrl('/apps/theming/styles')); + // Get all theming themes css links and force reload them + [...document.querySelectorAll('link.theme')] + .forEach(theme => { + // Only edit the clone to not remove applied one + var clone = theme.cloneNode() + var url = new URL(clone.href) + // Set current timestamp as cache buster + url.searchParams.set('v', Date.now()) + clone.href = url.toString() + clone.onload = function() { + theme.remove() + } + document.head.append(clone) + }) if (setting === 'name') { window.document.title = t('core', 'Admin') + " - " + value; } - + + // Finish + $('#theming_settings_loading').hide(); + var response = { status: 'success', data: {message: t('theming', 'Saved')}}; + OC.msg.finishedSaving('#theming_settings_msg', response); hideUndoButton(setting, value); - } function hideUndoButton(setting, value) { diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index a735dfafc2c..e8f6bd430d3 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -37,12 +37,13 @@ */ namespace OCA\Theming\Controller; -use OC\Template\SCSSCacher; use OCA\Theming\ImageManager; +use OCA\Theming\Service\ThemesService; use OCA\Theming\ThemingDefaults; use OCP\App\IAppManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; use OCP\AppFramework\Http\NotFoundResponse; @@ -63,40 +64,16 @@ use OCP\IURLGenerator; * @package OCA\Theming\Controller */ class ThemingController extends Controller { - /** @var ThemingDefaults */ - private $themingDefaults; - /** @var IL10N */ - private $l10n; - /** @var IConfig */ - private $config; - /** @var ITempManager */ - private $tempManager; - /** @var IAppData */ - private $appData; - /** @var SCSSCacher */ - private $scssCacher; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IAppManager */ - private $appManager; - /** @var ImageManager */ - private $imageManager; + private ThemingDefaults $themingDefaults; + private IL10N $l10n; + private IConfig $config; + private ITempManager $tempManager; + private IAppData $appData; + private IURLGenerator $urlGenerator; + private IAppManager $appManager; + private ImageManager $imageManager; + private ThemesService $themesService; - /** - * ThemingController constructor. - * - * @param string $appName - * @param IRequest $request - * @param IConfig $config - * @param ThemingDefaults $themingDefaults - * @param IL10N $l - * @param ITempManager $tempManager - * @param IAppData $appData - * @param SCSSCacher $scssCacher - * @param IURLGenerator $urlGenerator - * @param IAppManager $appManager - * @param ImageManager $imageManager - */ public function __construct( $appName, IRequest $request, @@ -105,10 +82,10 @@ class ThemingController extends Controller { IL10N $l, ITempManager $tempManager, IAppData $appData, - SCSSCacher $scssCacher, IURLGenerator $urlGenerator, IAppManager $appManager, - ImageManager $imageManager + ImageManager $imageManager, + ThemesService $themesService ) { parent::__construct($appName, $request); @@ -117,10 +94,10 @@ class ThemingController extends Controller { $this->config = $config; $this->tempManager = $tempManager; $this->appData = $appData; - $this->scssCacher = $scssCacher; $this->urlGenerator = $urlGenerator; $this->appManager = $appManager; $this->imageManager = $imageManager; + $this->themesService = $themesService; } /** @@ -185,19 +162,12 @@ class ThemingController extends Controller { $this->themingDefaults->set($setting, $value); - // reprocess server scss for preview - $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core'); - - return new DataResponse( - [ - 'data' => - [ - 'message' => $this->l10n->t('Saved'), - 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss')) - ], - 'status' => 'success' - ] - ); + return new DataResponse([ + 'data' => [ + 'message' => $this->l10n->t('Saved'), + ], + 'status' => 'success' + ]); } /** @@ -262,7 +232,6 @@ class ThemingController extends Controller { } $name = $image['name']; - $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core'); return new DataResponse( [ @@ -271,7 +240,6 @@ class ThemingController extends Controller { 'name' => $name, 'url' => $this->imageManager->getImageUrl($key), 'message' => $this->l10n->t('Saved'), - 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss')) ], 'status' => 'success' ] @@ -288,8 +256,6 @@ class ThemingController extends Controller { */ public function undo(string $setting): DataResponse { $value = $this->themingDefaults->undo($setting); - // reprocess server scss for preview - $cssCached = $this->scssCacher->process(\OC::$SERVERROOT, 'core/css/css-variables.scss', 'core'); return new DataResponse( [ @@ -297,7 +263,6 @@ class ThemingController extends Controller { [ 'value' => $value, 'message' => $this->l10n->t('Saved'), - 'serverCssUrl' => $this->urlGenerator->linkTo('', $this->scssCacher->getCachedSCSS('core', '/core/css/css-variables.scss')) ], 'status' => 'success' ] @@ -341,25 +306,31 @@ class ThemingController extends Controller { * @NoSameSiteCookieRequired * * @return FileDisplayResponse|NotFoundResponse - * @throws NotPermittedException - * @throws \Exception - * @throws \OCP\App\AppPathNotFoundException */ - public function getStylesheet() { - $appPath = $this->appManager->getAppPath('theming'); - - /* SCSSCacher is required here - * We cannot rely on automatic caching done by \OC_Util::addStyle, - * since we need to add the cacheBuster value to the url - */ - $cssCached = $this->scssCacher->process($appPath, 'css/theming.scss', 'theming'); - if (!$cssCached) { + public function getThemeVariables(string $themeId, bool $plain = false) { + $themes = $this->themesService->getThemes(); + if (!in_array($themeId, array_keys($themes))) { return new NotFoundResponse(); } + $theme = $themes[$themeId]; + + // Generate variables + $variables = ''; + foreach ($theme->getCSSVariables() as $variable => $value) { + $variables .= "$variable:$value; "; + }; + + // If plain is set, the browser decides of the css priority + if ($plain) { + $css = ":root { $variables }"; + } else { + // If not set, we'll rely on the body class + $css = "body[data-theme-$themeId] { $variables }"; + } + try { - $cssFile = $this->scssCacher->getCachedCSS('theming', 'theming.css'); - $response = new FileDisplayResponse($cssFile, Http::STATUS_OK, ['Content-Type' => 'text/css']); + $response = new DataDisplayResponse($css, Http::STATUS_OK, ['Content-Type' => 'text/css']); $response->cacheFor(86400); return $response; } catch (NotFoundException $e) { diff --git a/apps/theming/lib/ITheme.php b/apps/theming/lib/ITheme.php new file mode 100644 index 00000000000..7f3e49075ca --- /dev/null +++ b/apps/theming/lib/ITheme.php @@ -0,0 +1,55 @@ + + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see + * + */ + +namespace OCA\Theming; + +/** + * Interface ITheme + * + * @since 25.0.0 + */ +interface ITheme { + + /** + * Unique theme id + * @since 25.0.0 + */ + public function getId(): string; + + /** + * Get the media query triggering this theme + * Optional, ignored if falsy + * + * @return string + * @since 25.0.0 + */ + public function getMediaQuery(): string; + + /** + * Return the list of changed css variables + * + * @return array + * @since 25.0.0 + */ + public function getCSSVariables(): array; +} diff --git a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php index 10a9434835c..6842a731b5f 100644 --- a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php +++ b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php @@ -27,6 +27,8 @@ namespace OCA\Theming\Listener; use OCA\Theming\AppInfo\Application; use OCA\Theming\Service\JSDataService; +use OCA\Theming\Service\ThemeInjectionService; +use OCA\Theming\Service\ThemesService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\IConfig; @@ -36,25 +38,18 @@ use OCP\IURLGenerator; class BeforeTemplateRenderedListener implements IEventListener { - /** @var IInitialStateService */ - private $initialStateService; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IConfig */ - private $config; - /** @var IServerContainer */ - private $serverContainer; + private IInitialStateService $initialStateService; + private IServerContainer $serverContainer; + private ThemeInjectionService $themeInjectionService; public function __construct( IInitialStateService $initialStateService, - IURLGenerator $urlGenerator, - IConfig $config, - IServerContainer $serverContainer + IServerContainer $serverContainer, + ThemeInjectionService $themeInjectionService ) { $this->initialStateService = $initialStateService; - $this->urlGenerator = $urlGenerator; - $this->config = $config; $this->serverContainer = $serverContainer; + $this->themeInjectionService = $themeInjectionService; } public function handle(Event $event): void { @@ -63,19 +58,21 @@ class BeforeTemplateRenderedListener implements IEventListener { return $serverContainer->query(JSDataService::class); }); - $linkToCSS = $this->urlGenerator->linkToRoute( - 'theming.Theming.getStylesheet', - [ - 'v' => $this->config->getAppValue('theming', 'cachebuster', '0'), - ] - ); - \OCP\Util::addHeader( - 'link', - [ - 'rel' => 'stylesheet', - 'href' => $linkToCSS, - ] - ); + // $linkToCSS = $this->urlGenerator->linkToRoute( + // 'theming.Theming.getStylesheet', + // [ + // 'v' => $this->config->getAppValue('theming', 'cachebuster', '0'), + // ] + // ); + // \OCP\Util::addHeader( + // 'link', + // [ + // 'rel' => 'stylesheet', + // 'href' => $linkToCSS, + // ] + // ); + + $this->themeInjectionService->injectHeaders(); // Making sure to inject just after core \OCP\Util::addScript('theming', 'theming', 'core'); diff --git a/apps/theming/lib/Service/ThemeInjectionService.php b/apps/theming/lib/Service/ThemeInjectionService.php new file mode 100644 index 00000000000..0b4890cd08b --- /dev/null +++ b/apps/theming/lib/Service/ThemeInjectionService.php @@ -0,0 +1,88 @@ + + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Service; + +use OCA\Theming\Themes\DefaultTheme; +use OCP\IURLGenerator; +use OCP\Util; + +class ThemeInjectionService { + + private IURLGenerator $urlGenerator; + private ThemesService $themesService; + private DefaultTheme $defaultTheme; + + public function __construct(IURLGenerator $urlGenerator, + ThemesService $themesService, + DefaultTheme $defaultTheme) { + $this->urlGenerator = $urlGenerator; + $this->themesService = $themesService; + $this->defaultTheme = $defaultTheme; + } + + public function injectHeaders() { + $themes = $this->themesService->getThemes(); + $defaultTheme = $themes[$this->defaultTheme->getId()]; + $mediaThemes = array_filter($themes, function($theme) { + // Check if the theme provides a media query + return (bool)$theme->getMediaQuery(); + }); + + // Default theme fallback + $this->addThemeHeader($defaultTheme->getId()); + + // Themes applied by media queries + foreach($mediaThemes as $theme) { + $this->addThemeHeader($theme->getId(), true, $theme->getMediaQuery()); + } + + // Themes + foreach($this->themesService->getThemes() as $theme) { + // Ignore default theme as already processed first + if ($theme->getId() === $this->defaultTheme->getId()) { + continue; + } + $this->addThemeHeader($theme->getId(), false); + } + } + + /** + * Inject theme header into rendered page + * + * @param string $themeId the theme ID + * @param bool $plain request the :root syntax + * @param string $media media query to use in the element + */ + private function addThemeHeader(string $themeId, bool $plain = true, string $media = null) { + $linkToCSS = $this->urlGenerator->linkToRoute('theming.Theming.getThemeVariables', [ + 'themeId' => $themeId, + 'plain' => $plain, + ]); + Util::addHeader('link', [ + 'rel' => 'stylesheet', + 'media' => $media, + 'href' => $linkToCSS, + 'class' => 'theme' + ]); + } +} diff --git a/apps/theming/lib/Service/ThemesService.php b/apps/theming/lib/Service/ThemesService.php new file mode 100644 index 00000000000..3092b3bcbb5 --- /dev/null +++ b/apps/theming/lib/Service/ThemesService.php @@ -0,0 +1,56 @@ + + * + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Service; + +use OCA\Theming\Themes\DefaultTheme; +use OCA\Theming\Themes\DarkTheme; +use OCA\Theming\Themes\DarkHighContrastTheme; +use OCA\Theming\Themes\HighContrastTheme; +use OCA\Theming\ITheme; + +class ThemesService { + + /** @var ITheme[] */ + private array $themesProviders; + + public function __construct(DefaultTheme $defaultTheme, + DarkTheme $darkTheme, + DarkHighContrastTheme $darkHighContrastTheme, + HighContrastTheme $highContrastTheme) { + // Register themes + $this->themesProviders = [ + $defaultTheme->getId() => $defaultTheme, + $darkTheme->getId() => $darkTheme, + $darkHighContrastTheme->getId() => $darkHighContrastTheme, + $highContrastTheme->getId() => $highContrastTheme, + ]; + } + + public function getThemes() { + return $this->themesProviders; + } + + public function getThemeVariables(string $id) { + return $this->themesProviders[$id]->getCSSVariables(); + } +} diff --git a/apps/theming/lib/Themes/DarkHighContrastTheme.php b/apps/theming/lib/Themes/DarkHighContrastTheme.php new file mode 100644 index 00000000000..1f00990c7de --- /dev/null +++ b/apps/theming/lib/Themes/DarkHighContrastTheme.php @@ -0,0 +1,47 @@ + + * + * @author Joas Schilling + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Themes; + +use OCA\Theming\ITheme; + +class DarkHighContrastTheme extends HighContrastTheme implements ITheme { + + public function getId(): string { + return 'dark-highcontrast'; + } + + public function getMediaQuery(): string { + return '(prefers-color-scheme: dark) and (prefers-contrast: more)'; + } + + public function getCSSVariables(): array { + $variables = parent::getCSSVariables(); + + // FIXME … + $variables = $variables; + + return $variables; + } +} diff --git a/apps/theming/lib/Themes/DarkTheme.php b/apps/theming/lib/Themes/DarkTheme.php new file mode 100644 index 00000000000..b7ec16aa56b --- /dev/null +++ b/apps/theming/lib/Themes/DarkTheme.php @@ -0,0 +1,75 @@ + + * + * @author Joas Schilling + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Themes; + +use OCA\Theming\ITheme; + +class DarkTheme extends DefaultTheme implements ITheme { + + public function getId(): string { + return 'dark'; + } + + public function getMediaQuery(): string { + return '(prefers-color-scheme: dark)'; + } + + public function getCSSVariables(): array { + $defaultVariables = parent::getCSSVariables(); + + $colorMainText = '#D8D8D8'; + $colorMainBackground = '#171717'; + $colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground)); + $colorBoxShadow = $this->util->darken($colorMainBackground, 70); + $colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow)); + + return array_merge($defaultVariables, [ + '--color-main-text' => $colorMainText, + '--color-main-background' => $colorMainBackground, + '--color-main-background-rgb' => $colorMainBackgroundRGB, + + '--color-background-hover' => $this->util->lighten($colorMainBackground, 4), + '--color-background-dark' => $this->util->lighten($colorMainBackground, 7), + '--color-background-darker' => $this->util->lighten($colorMainBackground, 14), + + '--color-placeholder-light' => $this->util->lighten($colorMainBackground, 10), + '--color-placeholder-dark' => $this->util->lighten($colorMainBackground, 20), + + '--color-text-maxcontrast' => $this->util->darken($colorMainText, 30), + '--color-text-light' => $this->util->darken($colorMainText, 10), + '--color-text-lighter' => $this->util->darken($colorMainText, 20), + + '--color-loading-light' => '#777', + '--color-loading-dark' => '#CCC', + + '--color-box-shadow-rgb' => $colorBoxShadowRGB, + + '--color-border' => $this->util->lighten($colorMainBackground, 7), + '--color-border-dark' => $this->util->lighten($colorMainBackground, 14), + + '--background-invert-if-bright' => 'invert(100%)', + ]); + } +} diff --git a/apps/theming/lib/Themes/DefaultTheme.php b/apps/theming/lib/Themes/DefaultTheme.php new file mode 100644 index 00000000000..97650bf6292 --- /dev/null +++ b/apps/theming/lib/Themes/DefaultTheme.php @@ -0,0 +1,160 @@ + + * + * @author Joas Schilling + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Themes; + +use OCA\Theming\ThemingDefaults; +use OCA\Theming\Util; +use OCA\Theming\ITheme; +use OCP\IURLGenerator; + +class DefaultTheme implements ITheme { + public Util $util; + public ThemingDefaults $themingDefaults; + public IURLGenerator $urlGenerator; + public string $primaryColor; + + public function __construct(Util $util, ThemingDefaults $themingDefaults, IURLGenerator $urlGenerator) { + $this->util = $util; + $this->themingDefaults = $themingDefaults; + $this->urlGenerator = $urlGenerator; + + $this->primaryColor = $this->themingDefaults->getColorPrimary(); + } + + public function getId(): string { + return 'default'; + } + + public function getMediaQuery(): string { + return ''; + } + + public function getCSSVariables(): array { + $colorMainText = '#222222'; + $colorMainBackground = '#ffffff'; + $colorMainBackgroundRGB = join(',', $this->util->hexToRGB($colorMainBackground)); + $colorBoxShadow = $this->util->darken($colorMainBackground, 70); + $colorBoxShadowRGB = join(',', $this->util->hexToRGB($colorBoxShadow)); + + // Logo variables + $logoSvgPath = $this->urlGenerator->getAbsoluteURL($this->themingDefaults->getLogo()); + $backgroundSvgPath = $this->urlGenerator->getAbsoluteURL($this->themingDefaults->getBackground()); + + return [ + '--color-main-background' => $colorMainBackground, + '--color-main-background-rgb' => $colorMainBackgroundRGB, + '--color-main-background-translucent' => 'rgba(var(--color-main-background-rgb), .97)', + + // to use like this: background-image: linear-gradient(0, var('--gradient-main-background)); + '--gradient-main-background' => 'var(--color-main-background) 0%, var(--color-main-background-translucent) 85%, transparent 100%', + + // used for different active/hover/focus/disabled states + '--color-background-hover' => $this->util->darken($colorMainBackground, 4), + '--color-background-dark' => $this->util->darken($colorMainBackground, 7), + '--color-background-darker' => $this->util->darken($colorMainBackground, 14), + + '--color-placeholder-light' => $this->util->darken($colorMainBackground, 10), + '--color-placeholder-dark' => $this->util->darken($colorMainBackground, 20), + + // primary related colours + '--color-primary' => $this->primaryColor, + '--color-primary-text' => $this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff', + '--color-primary-hover' => $this->util->mix($this->primaryColor, $colorMainBackground, 80), + '--color-primary-light' => $this->util->mix($this->primaryColor, $colorMainBackground, 10), + '--color-primary-light-text' => $this->primaryColor, + '--color-primary-light-hover' => $this->util->mix($this->primaryColor, $colorMainText, 10), + '--color-primary-text-dark' => $this->util->darken($this->util->invertTextColor($this->primaryColor) ? '#000000' : '#ffffff', 7), + // used for buttons, inputs... + '--color-primary-element' => $this->util->elementColor($this->primaryColor), + '--color-primary-element-hover' => $this->util->mix($this->util->elementColor($this->primaryColor), $colorMainBackground, 80), + '--color-primary-element-light' => $this->util->lighten($this->util->elementColor($this->primaryColor), 15), + '--color-primary-element-lighter' => $this->util->mix($this->util->elementColor($this->primaryColor), $colorMainBackground, 15), + + // max contrast for WCAG compliance + '--color-main-text' => $colorMainText, + '--color-text-maxcontrast' => $this->util->lighten($colorMainText, 33), + '--color-text-light' => $colorMainText, + '--color-text-lighter' => $this->util->lighten($colorMainText, 33), + + // info/warning/success feedback colours + '--color-error' => '#e9322d', + '--color-error-hover' => $this->util->mix('#e9322d', $colorMainBackground, 80), + '--color-warning' => '#eca700', + '--color-warning-hover' => $this->util->mix('#eca700', $colorMainBackground, 80), + '--color-success' => '#46ba61', + '--color-success-hover' => $this->util->mix('#46ba61', $colorMainBackground, 80), + + // used for the icon loading animation + '--color-loading-light' => '#cccccc', + '--color-loading-dark' => '#444444', + + '--color-box-shadow-rgb' => $colorBoxShadowRGB, + '--color-box-shadow' => "rgba(var(--color-box-shadow-rgb), 0.5)", + + '--color-border' => $this->util->darken($colorMainBackground, 7), + '--color-border-dark' => $this->util->darken($colorMainBackground, 14), + + '--font-face' => "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Cantarell, Ubuntu, 'Helvetica Neue', Arial, sans-serif, 'Noto Color Emoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'", + '--default-font-size' => '15px', + + // TODO: support "(prefers-reduced-motion)" + '--animation-quick' => '100ms', + '--animation-slow' => '300ms', + + // Default variables -------------------------------------------- + '--image-logo' => "url('$logoSvgPath')", + '--image-login' => "url('$backgroundSvgPath')", + '--image-logoheader' => "url('$logoSvgPath')", + '--image-favicon' => "url('$logoSvgPath')", + + '--border-radius' => '3px', + '--border-radius-large' => '10px', + // pill-style button, value is large so big buttons also have correct roundness + '--border-radius-pill' => '100px', + + '--default-line-height' => '24px', + + // various structure data + '--header-height' => '50px', + '--navigation-width' => '300px', + '--sidebar-min-width' => '300px', + '--sidebar-max-width' => '500px', + '--list-min-width' => '200px', + '--list-max-width' => '300px', + '--header-menu-item-height' => '44px', + '--header-menu-profile-item-height' => '66px', + + // mobile. Keep in sync with core/js/js.js + '--breakpoint-mobile' => '1024px', + + // invert filter if primary is too bright + // to be used for legacy reasons only. Use inline + // svg with proper css variable instead or material + // design icons. + '--primary-invert-if-bright' => $this->util->invertTextColor($this->primaryColor) ? 'invert(100%)' : 'unset', + '--background-invert-if-bright' => 'unset', + ]; + } +} diff --git a/apps/theming/lib/Themes/HighContrastTheme.php b/apps/theming/lib/Themes/HighContrastTheme.php new file mode 100644 index 00000000000..cae7cc5be98 --- /dev/null +++ b/apps/theming/lib/Themes/HighContrastTheme.php @@ -0,0 +1,47 @@ + + * + * @author Joas Schilling + * @author John Molakvoæ + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OCA\Theming\Themes; + +use OCA\Theming\ITheme; + +class HighContrastTheme extends DefaultTheme implements ITheme { + + public function getId(): string { + return 'highcontrast'; + } + + public function getMediaQuery(): string { + return '(prefers-contrast: more)'; + } + + public function getCSSVariables(): array { + $variables = parent::getCSSVariables(); + + // FIXME … + $variables = $variables; + + return $variables; + } +} diff --git a/apps/theming/lib/Util.php b/apps/theming/lib/Util.php index 05b954d5059..beaca679149 100644 --- a/apps/theming/lib/Util.php +++ b/apps/theming/lib/Util.php @@ -34,17 +34,13 @@ use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Files\SimpleFS\ISimpleFile; use OCP\IConfig; +use Mexitek\PHPColors\Color; class Util { - /** @var IConfig */ - private $config; - - /** @var IAppManager */ - private $appManager; - - /** @var IAppData */ - private $appData; + private IConfig $config; + private IAppManager $appManager; + private IAppData $appData; /** * Util constructor. @@ -95,6 +91,21 @@ class Util { return $color; } + public function mix(string $color1, string $color2, int $factor): string { + $color = new Color($color1); + return '#' . $color->mix($color2, $factor); + } + + public function lighten(string $color, int $factor): string { + $color = new Color($color); + return '#' . $color->lighten($factor); + } + + public function darken(string $color, int $factor): string { + $color = new Color($color); + return '#' . $color->darken($factor); + } + /** * Convert RGB to HSL * @@ -106,38 +117,16 @@ class Util { * * @return array */ - public function toHSL($red, $green, $blue) { - $min = min($red, $green, $blue); - $max = max($red, $green, $blue); - $l = $min + $max; - $d = $max - $min; - - if ((int) $d === 0) { - $h = $s = 0; - } else { - if ($l < 255) { - $s = $d / $l; - } else { - $s = $d / (510 - $l); - } - - if ($red == $max) { - $h = 60 * ($green - $blue) / $d; - } elseif ($green == $max) { - $h = 60 * ($blue - $red) / $d + 120; - } else { - $h = 60 * ($red - $green) / $d + 240; - } - } - - return [fmod($h, 360), $s * 100, $l / 5.1]; + public function toHSL(string $red, string $green, string $blue): array { + $color = new Color(Color::rgbToHex(['R' => $red, 'G' => $green, 'B' => $blue])); + return array_values($color->getHsl()); } /** * @param string $color rgb color value * @return float */ - public function calculateLuminance($color) { + public function calculateLuminance(string $color): float { [$red, $green, $blue] = $this->hexToRGB($color); $hsl = $this->toHSL($red, $green, $blue); return $hsl[2] / 100; @@ -147,7 +136,7 @@ class Util { * @param string $color rgb color value * @return float */ - public function calculateLuma($color) { + public function calculateLuma(string $color): float { [$red, $green, $blue] = $this->hexToRGB($color); return (0.2126 * $red + 0.7152 * $green + 0.0722 * $blue) / 255; } @@ -157,19 +146,9 @@ class Util { * @return int[] * @psalm-return array{0: int, 1: int, 2: int} */ - public function hexToRGB($color) { - $hex = preg_replace("/[^0-9A-Fa-f]/", '', $color); - if (strlen($hex) === 3) { - $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; - } - if (strlen($hex) !== 6) { - return [0, 0, 0]; - } - return [ - hexdec(substr($hex, 0, 2)), - hexdec(substr($hex, 2, 2)), - hexdec(substr($hex, 4, 2)) - ]; + public function hexToRGB(string $color): array { + $color = new Color($color); + return array_values($color->getRgb()); } /** diff --git a/composer.json b/composer.json index 69ac5821c92..929ded70796 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,8 @@ "ext-pdo": "*", "ext-simplexml": "*", "ext-xmlreader": "*", - "ext-zip": "*" + "ext-zip": "*", + "mexitek/phpcolors": "^1.0" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.4" diff --git a/composer.lock b/composer.lock index 0d00ed2efcf..69042da683c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,62 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8333c8a239fe5ccec285dfbccc17cca4", - "packages": [], + "content-hash": "8f8b099365b3839a80b19e266c4ba5e4", + "packages": [ + { + "name": "mexitek/phpcolors", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/mexitek/phpColors.git", + "reference": "4043974240ca7dc3c2bec3c158588148b605b206" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mexitek/phpColors/zipball/4043974240ca7dc3c2bec3c158588148b605b206", + "reference": "4043974240ca7dc3c2bec3c158588148b605b206", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0" + }, + "require-dev": { + "nette/tester": "^2.3", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arlo Carreon", + "homepage": "http://arlocarreon.com", + "role": "creator" + } + ], + "description": "A series of methods that let you manipulate colors. Just incase you ever need different shades of one color on the fly.", + "homepage": "http://mexitek.github.com/phpColors/", + "keywords": [ + "color", + "css", + "design", + "frontend", + "ui" + ], + "support": { + "issues": "https://github.com/mexitek/phpColors/issues", + "source": "https://github.com/mexitek/phpColors" + }, + "time": "2021-11-26T13:19:08+00:00" + } + ], "packages-dev": [ { "name": "bamarni/composer-bin-plugin", diff --git a/core/css/apps.scss b/core/css/apps.scss index 15742786b0e..b683ffae0ef 100644 --- a/core/css/apps.scss +++ b/core/css/apps.scss @@ -285,6 +285,8 @@ kbd { margin-right: 11px; width: 16px; height: 16px; + // Legacy invert if bright background + filter: var(--background-invert-if-bright); } /* counter can also be inside the link */ diff --git a/core/css/header.scss b/core/css/header.scss index 27a8fe289fa..2c1dc647189 100644 --- a/core/css/header.scss +++ b/core/css/header.scss @@ -449,6 +449,11 @@ nav[role='navigation'] { // Make sure most app names don’t ellipsize letter-spacing: -0.5px; font-size: 12px; + + // If the primary is too bright, invert the app icons + svg image { + filter: var(--primary-invert-if-bright); + } } /* focused app visual feedback */ diff --git a/core/css/styles.scss b/core/css/styles.scss index 8a15cfa19d8..27e5675b53a 100644 --- a/core/css/styles.scss +++ b/core/css/styles.scss @@ -975,6 +975,7 @@ span.ui-icon { background-size: 20px 20px; padding: 14px; cursor: pointer; + filter: var(--primary-invert-if-bright); &:hover, &:focus, diff --git a/core/templates/layout.user.php b/core/templates/layout.user.php index 2b84c89fca6..f5ac783b340 100644 --- a/core/templates/layout.user.php +++ b/core/templates/layout.user.php @@ -64,7 +64,7 @@ $getUserAvatar = static function (int $size) use ($_): string { -