diff options
author | Joas Schilling <coding@schilljs.com> | 2022-03-29 23:18:40 +0300 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2022-04-21 10:29:33 +0300 |
commit | b3cf312edcefec3fb26bad8637f3a0969504be87 (patch) | |
tree | 43397a079ea2b558e23cef722d6abbca11bc7cfb /apps | |
parent | 12ed5c9ff3e9dac25b43a1ad934a97a86037000b (diff) |
Start theming providers
Signed-off-by: Joas Schilling <coding@schilljs.com>
Diffstat (limited to 'apps')
-rw-r--r-- | apps/theming/appinfo/routes.php | 4 | ||||
-rw-r--r-- | apps/theming/css/settings-admin.scss | 2 | ||||
-rw-r--r-- | apps/theming/js/settings-admin.js | 52 | ||||
-rw-r--r-- | apps/theming/lib/Controller/ThemingController.php | 109 | ||||
-rw-r--r-- | apps/theming/lib/ITheme.php | 55 | ||||
-rw-r--r-- | apps/theming/lib/Listener/BeforeTemplateRenderedListener.php | 49 | ||||
-rw-r--r-- | apps/theming/lib/Service/ThemeInjectionService.php | 88 | ||||
-rw-r--r-- | apps/theming/lib/Service/ThemesService.php | 56 | ||||
-rw-r--r-- | apps/theming/lib/Themes/DarkHighContrastTheme.php | 47 | ||||
-rw-r--r-- | apps/theming/lib/Themes/DarkTheme.php | 75 | ||||
-rw-r--r-- | apps/theming/lib/Themes/DefaultTheme.php | 160 | ||||
-rw-r--r-- | apps/theming/lib/Themes/HighContrastTheme.php | 47 | ||||
-rw-r--r-- | apps/theming/lib/Util.php | 75 |
13 files changed, 643 insertions, 176 deletions
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 = $("<link/>", { - 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 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/> + * + */ + +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 @@ +<?php +/** + * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +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 <link> 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 @@ +<?php +/** + * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> + * + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +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 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +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 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +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 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +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 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * @author John Molakvoæ <skjnldsv@protonmail.com> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ +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()); } /** |