diff options
author | John Molakvoæ <skjnldsv@protonmail.com> | 2022-10-20 13:18:06 +0200 |
---|---|---|
committer | John Molakvoæ <skjnldsv@protonmail.com> | 2022-11-29 11:22:13 +0100 |
commit | cedae7c6d74e11c8aaa59b09a38db04dbebc818d (patch) | |
tree | b95c77675542e0654084dd41f5d1f07a413b4db7 /apps | |
parent | a884f311b78341612adeb6d62f707dda1bae39e7 (diff) | |
download | nextcloud-server-cedae7c6d74e11c8aaa59b09a38db04dbebc818d.tar.gz nextcloud-server-cedae7c6d74e11c8aaa59b09a38db04dbebc818d.zip |
Allow to remove the background and select a custom colour
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
Diffstat (limited to 'apps')
18 files changed, 580 insertions, 240 deletions
diff --git a/apps/theming/appinfo/routes.php b/apps/theming/appinfo/routes.php index 0b51b22dbb7..eceb447620d 100644 --- a/apps/theming/appinfo/routes.php +++ b/apps/theming/appinfo/routes.php @@ -88,6 +88,11 @@ return [ 'url' => '/background/{type}', 'verb' => 'POST', ], + [ + 'name' => 'userTheme#deleteBackground', + 'url' => '/background/custom', + 'verb' => 'DELETE', + ], ], 'ocs' => [ [ diff --git a/apps/theming/css/settings-admin.css b/apps/theming/css/settings-admin.css new file mode 100644 index 00000000000..1979387c5dd --- /dev/null +++ b/apps/theming/css/settings-admin.css @@ -0,0 +1,148 @@ +#theming input { + width: 230px; +} +#theming input:focus, +#theming input:active { + padding-right: 30px; +} +#theming .fileupload { + display: none; +} +#theming div > label { + position: relative; +} +#theming .theme-undo { + position: absolute; + top: -7px; + right: 4px; + cursor: pointer; + opacity: 0.3; + padding: 7px; + vertical-align: top; + display: inline-block; + visibility: hidden; + height: 32px; + width: 32px; +} +#theming form.uploadButton { + width: 411px; + display: flex; + align-items: center; +} +#theming form .theme-undo, +#theming .theme-remove-bg { + cursor: pointer; + opacity: 0.3; + padding: 7px; + vertical-align: top; + display: inline-block; + float: right; + position: relative; + top: 4px; + right: 0px; + visibility: visible; + height: 32px; + width: 32px; + margin-left: auto; +} +#theming form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg { + margin-left: 0; +} +#theming input[type=text]:hover + .theme-undo, +#theming input[type=text] + .theme-undo:hover, +#theming input[type=text]:focus + .theme-undo, +#theming input[type=text]:active + .theme-undo, +#theming input[type=url]:hover + .theme-undo, +#theming input[type=url] + .theme-undo:hover, +#theming input[type=url]:focus + .theme-undo, +#theming input[type=url]:active + .theme-undo { + visibility: visible; +} +#theming label span { + display: inline-block; + min-width: 175px; + max-width: 175px; + white-space: wrap; + padding: 8px 0px; + vertical-align: top; +} +#theming .icon-upload, +#theming .uploadButton .icon-loading-small { + padding: 8px 20px; + width: 20px; + margin: 2px 0px; + min-height: 32px; + display: inline-block; +} +#theming #theming_settings_status { + height: 26px; + margin: 10px; +} +#theming #theming_settings_loading { + display: inline-block; + vertical-align: middle; + margin-right: 10px; +} +#theming #theming_settings_msg { + vertical-align: middle; + border-radius: 3px; +} +#theming #theming-preview { + width: 230px; + height: 140px; + background-size: cover; + background-position: center center; + text-align: center; + margin-left: 178px; + margin-top: 10px; + margin-bottom: 20px; + cursor: pointer; + background-color: var(--color-primary-default); + background-image: var(--image-background-default, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))); +} +#theming #theming-preview #theming-preview-logo { + cursor: pointer; + width: 20%; + height: 20%; + margin-top: 20px; + display: inline-block; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + background-image: var(--image-logo, url("../../../core/img/logo/logo.svg")); +} +#theming .theming-hints { + margin-top: 20px; +} +#theming .image-preview { + display: inline-block; + width: 80px; + height: 36px; + background-position: center; + background-repeat: no-repeat; + background-size: contain; +} +#theming #theming-preview-logoheader { + background-image: var(--image-logoheader); +} +#theming #theming-preview-favicon { + background-image: var(--image-favicon); +} +#theming #user-theming { + margin-top: 44px; + display: flex; +} +#theming #user-theming > div { + max-width: 400px; + margin-bottom: 44px; +} + +/* transition effects for theming value changes */ +#header { + transition: background-color 500ms linear; +} +#header svg, #header img { + transition: 500ms filter linear; +} + +/*# sourceMappingURL=settings-admin.css.map */ diff --git a/apps/theming/css/settings-admin.scss b/apps/theming/css/settings-admin.scss new file mode 100644 index 00000000000..f34dea52698 --- /dev/null +++ b/apps/theming/css/settings-admin.scss @@ -0,0 +1,168 @@ +#theming { + input { + width: 230px; + } + + input:focus, + input:active { + padding-right: 30px; + } + + .fileupload { + display: none; + } + + div > label { + position: relative; + } + + .theme-undo { + position: absolute; + top: -7px; // input padding + right: 4px; // input right margin + border + cursor: pointer; + opacity: .3; + padding: 7px; + vertical-align: top; + display: inline-block; + visibility: hidden; + height: 32px; // height of input + width: 32px; // height of input + } + form.uploadButton { + width: 411px; + display: flex; + align-items: center; + } + form .theme-undo, + .theme-remove-bg { + cursor: pointer; + opacity: .3; + padding: 7px; + vertical-align: top; + display: inline-block; + float: right; + position: relative; + top: 4px; + right: 0px; + visibility: visible; + height: 32px; + width: 32px; + // right align + margin-left: auto; + } + form .theme-undo:not([style*="display:"]) ~ .theme-remove-bg { + // Only align the undo button if both are shown + margin-left: 0; + } + + input[type='text']:hover + .theme-undo, + input[type='text'] + .theme-undo:hover, + input[type='text']:focus + .theme-undo, + input[type='text']:active + .theme-undo, + input[type='url']:hover + .theme-undo, + input[type='url'] + .theme-undo:hover, + input[type='url']:focus + .theme-undo, + input[type='url']:active + .theme-undo{ + visibility: visible; + } + + label span { + display: inline-block; + min-width: 175px; + max-width: 175px; + white-space: wrap; + padding: 8px 0px; + vertical-align: top; + } + + .icon-upload, + .uploadButton .icon-loading-small { + padding: 8px 20px; + width: 20px; + margin: 2px 0px; + min-height: 32px; + display: inline-block; + } + + #theming_settings_status { + height: 26px; + margin: 10px; + } + + #theming_settings_loading { + display: inline-block; + vertical-align: middle; + margin-right: 10px; + } + + #theming_settings_msg { + vertical-align: middle; + border-radius: 3px; + } + + #theming-preview { + width: 230px; + height: 140px; + background-size: cover; + background-position: center center; + text-align: center; + margin-left: 178px; + margin-top: 10px; + margin-bottom: 20px; + cursor: pointer; + background-color: var(--color-primary-default); + background-image: var(--image-background-default, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))); + + #theming-preview-logo { + cursor: pointer; + width: 20%; + height: 20%; + margin-top: 20px; + display: inline-block; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + background-image: var(--image-logo, url('../../../core/img/logo/logo.svg')); + } + } + + .theming-hints { + margin-top: 20px; + } + + .image-preview { + display: inline-block; + width: 80px; + height: 36px; + background-position: center; + background-repeat: no-repeat; + background-size: contain; + } + + #theming-preview-logoheader { + // Only using --image-logoheader to show the custom value only + background-image: var(--image-logoheader); + } + + #theming-preview-favicon { + background-image: var(--image-favicon); + } + + #user-theming { + margin-top: 44px; + display: flex; + & > div { + max-width: 400px; + margin-bottom: 44px; + } + } +} + +/* transition effects for theming value changes */ +#header { + transition: background-color 500ms linear; + svg, img { + transition: 500ms filter linear; + } +} diff --git a/apps/theming/lib/Command/UpdateConfig.php b/apps/theming/lib/Command/UpdateConfig.php index c327c92492f..58dfcff8a8e 100644 --- a/apps/theming/lib/Command/UpdateConfig.php +++ b/apps/theming/lib/Command/UpdateConfig.php @@ -36,10 +36,6 @@ class UpdateConfig extends Command { 'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'disable-user-theming' ]; - public const SUPPORTED_IMAGE_KEYS = [ - 'background', 'logo', 'favicon', 'logoheader' - ]; - private $themingDefaults; private $imageManager; private $config; @@ -87,14 +83,14 @@ class UpdateConfig extends Command { $value = $this->config->getAppValue('theming', $key, ''); $output->writeln('- ' . $key . ': ' . $value . ''); } - foreach (self::SUPPORTED_IMAGE_KEYS as $key) { + foreach (ImageManager::SUPPORTED_IMAGE_KEYS as $key) { $value = $this->config->getAppValue('theming', $key . 'Mime', ''); $output->writeln('- ' . $key . ': ' . $value . ''); } return 0; } - if (!in_array($key, self::SUPPORTED_KEYS, true) && !in_array($key, self::SUPPORTED_IMAGE_KEYS, true)) { + if (!in_array($key, self::SUPPORTED_KEYS, true) && !in_array($key, ImageManager::SUPPORTED_IMAGE_KEYS, true)) { $output->writeln('<error>Invalid config key provided</error>'); return 1; } @@ -115,7 +111,7 @@ class UpdateConfig extends Command { return 0; } - if (in_array($key, self::SUPPORTED_IMAGE_KEYS, true)) { + if (in_array($key, ImageManager::SUPPORTED_IMAGE_KEYS, true)) { if (strpos($value, '/') !== 0) { $output->writeln('<error>The image file needs to be provided as an absolute path: ' . $value . '.</error>'); return 1; diff --git a/apps/theming/lib/Controller/UserThemeController.php b/apps/theming/lib/Controller/UserThemeController.php index 635dad34736..888ab9a0ca8 100644 --- a/apps/theming/lib/Controller/UserThemeController.php +++ b/apps/theming/lib/Controller/UserThemeController.php @@ -155,21 +155,34 @@ class UserThemeController extends OCSController { /** * @NoAdminRequired */ - public function setBackground(string $type = 'default', string $value = ''): JSONResponse { + public function deleteBackground(): JSONResponse { + $currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0'); + $this->backgroundService->deleteBackgroundImage(); + return new JSONResponse([ + 'backgroundImage' => null, + 'backgroundColor' => $this->themingDefaults->getColorPrimary(), + 'version' => $currentVersion, + ]); + } + + /** + * @NoAdminRequired + */ + public function setBackground(string $type = BackgroundService::BACKGROUND_DEFAULT, string $value = ''): JSONResponse { $currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0'); try { switch ($type) { - case 'shipped': + case BackgroundService::BACKGROUND_SHIPPED: $this->backgroundService->setShippedBackground($value); break; - case 'custom': + case BackgroundService::BACKGROUND_CUSTOM: $this->backgroundService->setFileBackground($value); break; case 'color': $this->backgroundService->setColorBackground($value); break; - case 'default': + case BackgroundService::BACKGROUND_DEFAULT: $this->backgroundService->setDefaultBackground(); break; default: @@ -185,8 +198,8 @@ class UserThemeController extends OCSController { $this->config->setUserValue($this->userId, Application::APP_ID, 'userCacheBuster', (string)$currentVersion); return new JSONResponse([ - 'type' => $type, - 'value' => $value, + 'backgroundImage' => $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT), + 'backgroundColor' => $this->themingDefaults->getColorPrimary(), 'version' => $currentVersion, ]); } diff --git a/apps/theming/lib/ImageManager.php b/apps/theming/lib/ImageManager.php index 88a733580fc..ce9c2525802 100644 --- a/apps/theming/lib/ImageManager.php +++ b/apps/theming/lib/ImageManager.php @@ -33,6 +33,8 @@ */ namespace OCA\Theming; +use OCA\Theming\AppInfo\Application; +use OCA\Theming\Service\BackgroundService; use OCP\Files\IAppData; use OCP\Files\NotFoundException; use OCP\Files\NotPermittedException; @@ -45,7 +47,7 @@ use OCP\ITempManager; use OCP\IURLGenerator; class ImageManager { - public const SupportedImageKeys = ['background', 'logo', 'logoheader', 'favicon']; + public const SUPPORTED_IMAGE_KEYS = ['background', 'logo', 'logoheader', 'favicon']; /** @var IConfig */ private $config; @@ -74,8 +76,14 @@ class ImageManager { $this->appData = $appData; } - public function getImageUrl(string $key, bool $useSvg = true): string { - $cacheBusterCounter = $this->config->getAppValue('theming', 'cachebuster', '0'); + /** + * Get a globally defined image (admin theming settings) + * + * @param string $key the image key + * @return string the image url + */ + public function getImageUrl(string $key): string { + $cacheBusterCounter = $this->config->getAppValue(Application::APP_ID, 'cachebuster', '0'); if ($this->hasImage($key)) { return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter; } @@ -86,13 +94,16 @@ class ImageManager { case 'favicon': return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter; case 'background': - return $this->urlGenerator->imagePath('core', 'background.png') . '?v=' . $cacheBusterCounter; + return $this->urlGenerator->linkTo(Application::APP_ID, "img/background/" . BackgroundService::DEFAULT_BACKGROUND); } return ''; } - public function getImageUrlAbsolute(string $key, bool $useSvg = true): string { - return $this->urlGenerator->getAbsoluteURL($this->getImageUrl($key, $useSvg)); + /** + * Get the absolute url. See getImageUrl + */ + public function getImageUrlAbsolute(string $key): string { + return $this->urlGenerator->getAbsoluteURL($this->getImageUrl($key)); } /** @@ -137,6 +148,20 @@ class ImageManager { } /** + * @return array<string, array{mime: string, url: string}> + */ + public function getCustomImages(): array { + $images = []; + foreach (self::SUPPORTED_IMAGE_KEYS as $key) { + $images[$key] = [ + 'mime' => $this->config->getAppValue('theming', $key . 'Mime', ''), + 'url' => $this->getImageUrl($key), + ]; + } + return $images; + } + + /** * Get folder for current theming files * * @return ISimpleFolder diff --git a/apps/theming/lib/Jobs/MigrateBackgroundImages.php b/apps/theming/lib/Jobs/MigrateBackgroundImages.php index 54c0d591e40..4b0cf187bae 100644 --- a/apps/theming/lib/Jobs/MigrateBackgroundImages.php +++ b/apps/theming/lib/Jobs/MigrateBackgroundImages.php @@ -27,6 +27,7 @@ declare(strict_types=1); namespace OCA\Theming\Jobs; use OCA\Theming\AppInfo\Application; +use OCP\App\IAppManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\IJobList; use OCP\BackgroundJob\QueuedJob; diff --git a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php index a6e0923e643..d0fa9690602 100644 --- a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php +++ b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php @@ -84,16 +84,32 @@ class BeforeTemplateRenderedListener implements IEventListener { if (!empty($user)) { $userId = $user->getUID(); + /** User background */ $this->initialState->provideInitialState( - 'background', - $this->config->getUserValue($userId, Application::APP_ID, 'background', 'default'), + 'backgroundImage', + $this->config->getUserValue($userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT), ); + /** User color */ + $this->initialState->provideInitialState( + 'backgroundColor', + $this->config->getUserValue($userId, Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT), + ); + + /** + * Admin background. `backgroundColor` if disabled, + * mime type if defined and empty by default + */ $this->initialState->provideInitialState( 'themingDefaultBackground', $this->config->getAppValue('theming', 'backgroundMime', ''), ); + $this->initialState->provideInitialState( + 'defaultShippedBackground', + BackgroundService::DEFAULT_BACKGROUND, + ); + /** List of all shipped backgrounds */ $this->initialState->provideInitialState( 'shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS, diff --git a/apps/theming/lib/Service/BackgroundService.php b/apps/theming/lib/Service/BackgroundService.php index d49878b11b0..667ca99a1f9 100644 --- a/apps/theming/lib/Service/BackgroundService.php +++ b/apps/theming/lib/Service/BackgroundService.php @@ -48,6 +48,12 @@ class BackgroundService { public const DEFAULT_COLOR = '#0082c9'; public const DEFAULT_ACCESSIBLE_COLOR = '#006aa3'; + public const BACKGROUND_SHIPPED = 'shipped'; + public const BACKGROUND_CUSTOM = 'custom'; + public const BACKGROUND_DEFAULT = 'default'; + public const BACKGROUND_DISABLED = 'disabled'; + + public const DEFAULT_BACKGROUND = 'kamil-porembinski-clouds.jpg'; public const SHIPPED_BACKGROUNDS = [ 'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [ 'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)', @@ -153,7 +159,7 @@ class BackgroundService { } public function setDefaultBackground(): void { - $this->config->deleteUserValue($this->userId, Application::APP_ID, 'background'); + $this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image'); } /** @@ -165,7 +171,7 @@ class BackgroundService { * @throws NoUserException */ public function setFileBackground($path): void { - $this->config->setUserValue($this->userId, Application::APP_ID, 'background', 'custom'); + $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DEFAULT); $userFolder = $this->rootFolder->getUserFolder($this->userId); /** @var File $file */ @@ -183,27 +189,28 @@ class BackgroundService { if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) { throw new InvalidArgumentException('The given file name is invalid'); } - $this->config->setUserValue($this->userId, Application::APP_ID, 'background', $fileName); + $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', $fileName); + $this->setColorBackground(self::SHIPPED_BACKGROUNDS[$fileName]['primary_color']); } public function setColorBackground(string $color): void { if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) { throw new InvalidArgumentException('The given color is invalid'); } - $this->config->setUserValue($this->userId, Application::APP_ID, 'background', $color); + $this->config->setUserValue($this->userId, Application::APP_ID, 'background_color', $color); + } + + public function deleteBackgroundImage(): void { + $this->config->setUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DISABLED); } public function getBackground(): ?ISimpleFile { - $background = $this->config->getUserValue($this->userId, Application::APP_ID, 'background', 'default'); - if ($background === 'custom') { + $background = $this->config->getUserValue($this->userId, Application::APP_ID, 'background_image', self::BACKGROUND_DEFAULT); + if ($background === self::BACKGROUND_CUSTOM) { try { return $this->getAppDataFolder()->getFile('background.jpg'); } catch (NotFoundException | NotPermittedException $e) { - try { - // Fallback can be removed in 26 - $dashboardFolder = $this->appDataFactory->get('dashboard'); - return $dashboardFolder->getFolder($this->userId)->getFile('background.jpg'); - } catch (\Throwable $t) {} + return null; } } return null; diff --git a/apps/theming/lib/Themes/CommonThemeTrait.php b/apps/theming/lib/Themes/CommonThemeTrait.php index 360c335fc7d..c58a3fd43e3 100644 --- a/apps/theming/lib/Themes/CommonThemeTrait.php +++ b/apps/theming/lib/Themes/CommonThemeTrait.php @@ -88,6 +88,10 @@ trait CommonThemeTrait { $variables = []; + // Default last fallback values + $variables['--image-background-default'] = $backgroundDeleted ?: "url('" . $this->themingDefaults->getBackground() . "')"; + $variables['--color-background-plain'] = $this->defaultPrimaryColor; + // If primary as background has been request or if we have a custom primary colour // let's not define the background image if ($backgroundDeleted) { @@ -98,7 +102,7 @@ trait CommonThemeTrait { } // Register image variables only if custom-defined - foreach (ImageManager::SupportedImageKeys as $image) { + foreach (ImageManager::SUPPORTED_IMAGE_KEYS as $image) { if ($this->imageManager->hasImage($image)) { $imageUrl = $this->imageManager->getImageUrl($image); if ($image === 'background') { @@ -110,6 +114,7 @@ trait CommonThemeTrait { $variables['--image-background-size'] = 'cover'; $variables['--image-background-default'] = "url('" . $imageUrl . "')"; } + // --image-background is overriden by user theming $variables["--image-$image"] = "url('" . $imageUrl . "')"; } } @@ -129,32 +134,32 @@ trait CommonThemeTrait { if ($user !== null && !$this->themingDefaults->isUserThemingDisabled() && $this->appManager->isEnabledForUser(Application::APP_ID)) { - $themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background', 'default'); + $backgroundImage = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_image', BackgroundService::BACKGROUND_DEFAULT); $currentVersion = (int)$this->config->getUserValue($user->getUID(), Application::APP_ID, 'userCacheBuster', '0'); - // The user uploaded a custom background - if ($themingBackground === 'custom') { - $cacheBuster = substr(sha1($user->getUID() . '_' . $currentVersion), 0, 8); + // The user removed the background + if ($backgroundImage === BackgroundService::BACKGROUND_DISABLED) { return [ - '--image-background' => "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')", - // TODO: implement primary color from custom background --color-background-plain + '--image-background' => 'no', + '--color-background-plain' => $this->themingDefaults->getColorPrimary(), ]; } - // The user picked a shipped background - if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground])) { + // The user uploaded a custom background + if ($backgroundImage === BackgroundService::BACKGROUND_CUSTOM) { + $cacheBuster = substr(sha1($user->getUID() . '_' . $currentVersion), 0, 8); return [ - '--image-background' => "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "/img/background/$themingBackground") . "')", + '--image-background' => "url('" . $this->urlGenerator->linkToRouteAbsolute('theming.userTheme.getBackground') . "?v=$cacheBuster')", '--color-background-plain' => $this->themingDefaults->getColorPrimary(), - '--background-image-invert-if-bright' => BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['theming'] ?? null === BackgroundService::THEMING_MODE_DARK ? 'invert(100%)' : 'no', ]; } - // The user picked a static colour - if (substr($themingBackground, 0, 1) === '#') { + // The user picked a shipped background + if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage])) { return [ - '--image-background' => 'no', + '--image-background' => "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')", '--color-background-plain' => $this->themingDefaults->getColorPrimary(), + '--background-image-invert-if-bright' => BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage]['theming'] ?? null === BackgroundService::THEMING_MODE_DARK ? 'invert(100%)' : 'no', ]; } } diff --git a/apps/theming/lib/Themes/DefaultTheme.php b/apps/theming/lib/Themes/DefaultTheme.php index 85e437a82e5..100d34864fc 100644 --- a/apps/theming/lib/Themes/DefaultTheme.php +++ b/apps/theming/lib/Themes/DefaultTheme.php @@ -190,11 +190,6 @@ class DefaultTheme implements ITheme { '--background-invert-if-dark' => 'no', '--background-invert-if-bright' => 'invert(100%)', '--background-image-invert-if-bright' => 'no', - - // Default last fallback values - '--image-background' => "url('" . $this->urlGenerator->imagePath('core', 'app-background.jpg') . "')", - '--image-background-default' => "url('" . $this->urlGenerator->imagePath('core', 'app-background.jpg') . "')", - '--color-background-plain' => $this->defaultPrimaryColor, ]; // Primary variables diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index 16ea7a14c0f..42965ca6795 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -226,23 +226,12 @@ class ThemingDefaults extends \OC_Defaults { } // user-defined primary color - $themingBackground = ''; if (!empty($user)) { - $themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background', ''); - // If the user selected the default background - if ($themingBackground === '') { - return BackgroundService::DEFAULT_COLOR; - } - + $themingBackground = $this->config->getUserValue($user->getUID(), Application::APP_ID, 'background_color', ''); // If the user selected a specific colour if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackground)) { return $themingBackground; } - - // if the user-selected background is a background reference - if (isset(BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['primary_color'])) { - return BackgroundService::SHIPPED_BACKGROUNDS[$themingBackground]['primary_color']; - } } // If the default color is not valid, return the default background one @@ -477,7 +466,7 @@ class ThemingDefaults extends \OC_Defaults { $returnValue = $this->getSlogan(); break; case 'color': - $returnValue = $this->getColorPrimary(); + $returnValue = $this->getDefaultColorPrimary(); break; case 'logo': case 'logoheader': diff --git a/apps/theming/src/UserThemes.vue b/apps/theming/src/UserThemes.vue index 5a41c019017..7220820a3c3 100644 --- a/apps/theming/src/UserThemes.vue +++ b/apps/theming/src/UserThemes.vue @@ -69,10 +69,7 @@ </template> <template v-else> <p>{{ t('theming', 'Set a custom background') }}</p> - <BackgroundSettings class="background__grid" - :background="background" - :theming-default-background="themingDefaultBackground" - @update:background="updateBackground" /> + <BackgroundSettings class="background__grid" @update:background="refreshGlobalStyles" /> </template> </NcSettingsSection> </section> @@ -92,8 +89,6 @@ const availableThemes = loadState('theming', 'themes', []) const enforceTheme = loadState('theming', 'enforceTheme', '') const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false) -const background = loadState('theming', 'background') -const themingDefaultBackground = loadState('theming', 'themingDefaultBackground') const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled') console.debug('Available themes', availableThemes) @@ -111,10 +106,10 @@ export default { data() { return { availableThemes, + + // Admin defined configs enforceTheme, shortcutsDisabled, - background, - themingDefaultBackground, isUserThemingDisabled, } }, @@ -173,9 +168,21 @@ export default { }, methods: { + // Refresh server-side generated theming CSS + refreshGlobalStyles() { + [...document.head.querySelectorAll('link.theme')].forEach(theme => { + const url = new URL(theme.href) + url.searchParams.set('v', Date.now()) + const newTheme = theme.cloneNode() + newTheme.href = url.toString() + newTheme.onload = () => theme.remove() + document.head.append(newTheme) + }) + }, + updateBackground(data) { this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value - this.$emit('update:background') + this.refreshGlobalStyles() }, changeTheme({ enabled, id }) { diff --git a/apps/theming/src/components/BackgroundSettings.vue b/apps/theming/src/components/BackgroundSettings.vue index 45e627fd378..9890f9ad3f0 100644 --- a/apps/theming/src/components/BackgroundSettings.vue +++ b/apps/theming/src/components/BackgroundSettings.vue @@ -26,68 +26,75 @@ <template> <div class="background-selector"> <!-- Custom background --> - <button class="background filepicker" - :class="{ active: background === 'custom' }" + <button class="background background__filepicker" + :class="{ 'background--active': backgroundImage === 'custom' }" tabindex="0" @click="pickFile"> - {{ t('theming', 'Pick from Files') }} + {{ t('theming', 'Custom background') }} </button> <!-- Default background --> - <button class="background default" + <button class="background background__default" + :class="{ 'icon-loading': loading === 'default', 'background--active': backgroundImage === 'default' }" + :data-color-bright="invertTextColor(Theming.defaultColor)" + :style="{ '--border-color': Theming.defaultColor }" tabindex="0" - :class="{ 'icon-loading': loading === 'default', active: background === 'default' }" @click="setDefault"> - {{ t('theming', 'Default image') }} + {{ t('theming', 'Default background') }} + <Check :size="44" /> </button> <!-- Custom color picker --> <NcColorPicker v-model="Theming.color" @input="debouncePickColor"> - <button class="background color" - :class="{ active: background === Theming.color}" - tabindex="0" + <button class="background background__color" :data-color="Theming.color" :data-color-bright="invertTextColor(Theming.color)" - :style="{ backgroundColor: Theming.color, color: invertTextColor(Theming.color) ? '#000000' : '#ffffff'}"> - {{ t('theming', 'Custom color') }} + :style="{ backgroundColor: Theming.color, '--border-color': Theming.color}" + tabindex="0"> + {{ t('theming', 'Change color') }} </button> </NcColorPicker> - <!-- Default admin primary color --> - <button class="background color" - :class="{ active: background === Theming.defaultColor }" - tabindex="0" - :data-color="Theming.defaultColor" - :data-color-bright="invertTextColor(Theming.defaultColor)" - :style="{ color: invertTextColor(Theming.defaultColor) ? '#000000' : '#ffffff'}" - @click="debouncePickColor"> - {{ t('theming', 'Plain background') }} - </button> - <!-- Background set selection --> <button v-for="shippedBackground in shippedBackgrounds" :key="shippedBackground.name" v-tooltip="shippedBackground.details.attribution" - :class="{ 'icon-loading': loading === shippedBackground.name, active: background === shippedBackground.name }" - tabindex="0" - class="background" + :class="{ 'icon-loading': loading === shippedBackground.name, 'background--active': backgroundImage === shippedBackground.name }" :data-color-bright="shippedBackground.details.theming === 'dark'" - :style="{ 'background-image': 'url(' + shippedBackground.preview + ')' }" - @click="setShipped(shippedBackground.name)" /> + :style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }" + class="background background__shipped" + tabindex="0" + @click="setShipped(shippedBackground.name)"> + <Check :size="44" /> + </button> + + <!-- Remove background --> + <button class="background background__delete" + tabindex="0" + @click="removeBackground"> + {{ t('theming', 'Remove background') }} + <Close :size="24" /> + </button> </div> </template> <script> -import { generateUrl } from '@nextcloud/router' -import { getBackgroundUrl } from '../helpers/getBackgroundUrl.js' +import { generateFilePath, generateUrl } from '@nextcloud/router' import { loadState } from '@nextcloud/initial-state' -import { prefixWithBaseUrl } from '../helpers/prefixWithBaseUrl.js' import axios from '@nextcloud/axios' +import Check from 'vue-material-design-icons/Check.vue' +import Close from 'vue-material-design-icons/Close.vue' import debounce from 'debounce' import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker' import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' +const backgroundColor = loadState('theming', 'backgroundColor') +const backgroundImage = loadState('theming', 'backgroundImage') const shippedBackgroundList = loadState('theming', 'shippedBackgrounds') +const themingDefaultBackground = loadState('theming', 'themingDefaultBackground') +const defaultShippedBackground = loadState('theming', 'defaultShippedBackground') + +const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url export default { name: 'BackgroundSettings', @@ -96,38 +103,49 @@ export default { }, components: { + Check, + Close, NcColorPicker, }, - props: { - background: { - type: String, - default: 'default', - }, - themingDefaultBackground: { - type: String, - default: '', - }, - }, - data() { return { - backgroundImage: generateUrl('/apps/theming/background') + '?v=' + Date.now(), loading: false, Theming: loadState('theming', 'data', {}), + + // User background image and color settings + backgroundImage, + backgroundColor, } }, computed: { shippedBackgrounds() { - return Object.keys(shippedBackgroundList).map(fileName => { - return { - name: fileName, - url: prefixWithBaseUrl(fileName), - preview: prefixWithBaseUrl('preview/' + fileName), - details: shippedBackgroundList[fileName], - } - }) + return Object.keys(shippedBackgroundList) + .map(fileName => { + return { + name: fileName, + url: prefixWithBaseUrl(fileName), + preview: prefixWithBaseUrl('preview/' + fileName), + details: shippedBackgroundList[fileName], + } + }) + .filter(background => { + // If the admin did not changed the global background + // let's hide the default background to not show it twice + if (!this.isGlobalBackgroundDeleted && !this.isGlobalBackgroundDefault) { + return background.name !== defaultShippedBackground + } + return true + }) + }, + + isGlobalBackgroundDefault() { + return !!themingDefaultBackground + }, + + isGlobalBackgroundDeleted() { + return themingDefaultBackground === 'backgroundColor' }, }, @@ -163,20 +181,24 @@ export default { : null }, + /** + * Update local state + * + * @param {object} data destructuring object + * @param {string} data.backgroundColor background color value + * @param {string} data.backgroundImage background image value + * @param {string} data.version cache buster number + * @see https://github.com/nextcloud/server/blob/c78bd45c64d9695724fc44fe8453a88824b85f2f/apps/theming/lib/Controller/UserThemeController.php#L187-L191 + */ async update(data) { - const background = data.type === 'custom' || data.type === 'default' ? data.type : data.value - this.backgroundImage = getBackgroundUrl(background, data.version, this.themingDefaultBackground) - if (data.type === 'color' || (data.type === 'default' && this.themingDefaultBackground === 'backgroundColor')) { - this.$emit('update:background', data) - this.loading = false - return - } - const image = new Image() - image.onload = () => { - this.$emit('update:background', data) - this.loading = false - } - image.src = this.backgroundImage + // Update state + this.backgroundImage = data.backgroundImage + this.backgroundColor = data.backgroundColor + this.Theming.color = data.backgroundColor + + // Notify parent and reload style + this.$emit('update:background') + this.loading = false }, async setDefault() { @@ -197,15 +219,21 @@ export default { this.update(result.data) }, - debouncePickColor: debounce(function() { - this.pickColor(...arguments) - }, 200), + async removeBackground() { + this.loading = 'remove' + const result = await axios.delete(generateUrl('/apps/theming/background/custom')) + this.update(result.data) + }, + async pickColor(event) { this.loading = 'color' const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9' const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color }) this.update(result.data) }, + debouncePickColor: debounce(function() { + this.pickColor(...arguments) + }, 200), pickFile() { window.OC.dialogs.filepicker(t('theming', 'Select a background from your files'), (path, type) => { @@ -225,50 +253,61 @@ export default { justify-content: center; .background { + overflow: hidden; width: 176px; height: 96px; margin: 8px; - background-size: cover; - background-position: center center; text-align: center; - border-radius: var(--border-radius-large); border: 2px solid var(--color-main-background); - overflow: hidden; + border-radius: var(--border-radius-large); + background-position: center center; + background-size: cover; - &.current { - background-image: var(--color-background-dark); + &__default { + background-color: var(--color-primary-default); + background-image: var(--image-background-default); } - &.filepicker, &.default, &.color { + &__filepicker, &__default, &__color { border-color: var(--color-border); } - &.color { - background-color: var(--color-primary-default); + &__color { color: var(--color-primary-text); + background-color: var(--color-primary-default); + } + + // Text and svg icon dark on bright background + &[data-color-bright] { + color: black; } - &.active, + &--active, &:hover, &:focus { - border: 2px solid var(--color-primary); + // Use theme color primary, see inline css variable in template + border: 2px solid var(--border-color, var(--color-primary)) !important; } - &.active:not(.icon-loading) { - &:after { - background-image: var(--original-icon-checkmark-white); - background-repeat: no-repeat; - background-position: center; - background-size: 44px; - content: ''; - display: block; - height: 100%; + // Icon + span { + margin: 4px; + } + + &__default, + &__shipped { + color: white; + span { + display: none; } + } - &[data-color-bright]:after { - background-image: var(--original-icon-checkmark-dark); + &--active:not(.icon-loading) { + span { + display: block; } } } } + </style> diff --git a/apps/theming/src/helpers/getBackgroundUrl.js b/apps/theming/src/helpers/getBackgroundUrl.js deleted file mode 100644 index 88a3ab57291..00000000000 --- a/apps/theming/src/helpers/getBackgroundUrl.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Avior <florian.bouillon@delta-wings.net> - * @author Julien Veyssier <eneiluj@posteo.net> - * @author Julius Härtl <jus@bitgrid.net> - * - * @license AGPL-3.0-or-later - * - * 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/>. - * - */ - -import { generateUrl } from '@nextcloud/router' -import { prefixWithBaseUrl } from './prefixWithBaseUrl.js' - -export const getBackgroundUrl = (background, time = 0, themingDefaultBackground = '') => { - const enabledThemes = window.OCA?.Theming?.enabledThemes || [] - const isDarkTheme = (enabledThemes.length === 0 || enabledThemes[0] === 'default') - ? window.matchMedia('(prefers-color-scheme: dark)').matches - : enabledThemes.join('').indexOf('dark') !== -1 - - if (background === 'default') { - if (themingDefaultBackground && themingDefaultBackground !== 'backgroundColor') { - return generateUrl('/apps/theming/image/background') + '?v=' + window.OCA.Theming.cacheBuster - } - - if (isDarkTheme) { - return prefixWithBaseUrl('eduardo-neves-pedra-azul.jpg') - } - - return prefixWithBaseUrl('kamil-porembinski-clouds.jpg') - } else if (background === 'custom') { - return generateUrl('/apps/theming/background') + '?v=' + time - } - - return prefixWithBaseUrl(background) -} diff --git a/apps/theming/src/helpers/prefixWithBaseUrl.js b/apps/theming/src/helpers/prefixWithBaseUrl.js deleted file mode 100644 index d2f42c93549..00000000000 --- a/apps/theming/src/helpers/prefixWithBaseUrl.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license AGPL-3.0-or-later - * - * 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/>. - * - */ - -import { generateFilePath } from '@nextcloud/router' - -export const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php index 9042a338fb7..c67be434631 100644 --- a/apps/theming/tests/Controller/ThemingControllerTest.php +++ b/apps/theming/tests/Controller/ThemingControllerTest.php @@ -680,7 +680,7 @@ class ThemingControllerTest extends TestCase { public function testGetLoginBackground() { $file = $this->createMock(ISimpleFile::class); - $file->method('getName')->willReturn('background.png'); + $file->method('getName')->willReturn('app-background.jpg'); $file->method('getMTime')->willReturn(42); $this->imageManager->expects($this->once()) ->method('getImage') diff --git a/apps/theming/tests/ThemingDefaultsTest.php b/apps/theming/tests/ThemingDefaultsTest.php index fafa1fd450c..caed7002980 100644 --- a/apps/theming/tests/ThemingDefaultsTest.php +++ b/apps/theming/tests/ThemingDefaultsTest.php @@ -484,7 +484,7 @@ class ThemingDefaultsTest extends TestCase { $this->config ->expects($this->once()) ->method('getUserValue') - ->with('user', 'theming', 'background', '') + ->with('user', 'theming', 'background_image', '') ->willReturn(array_keys(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]); $this->config ->expects($this->exactly(2)) @@ -509,7 +509,7 @@ class ThemingDefaultsTest extends TestCase { $this->config ->expects($this->once()) ->method('getUserValue') - ->with('user', 'theming', 'background', '') + ->with('user', 'theming', 'background_image', '') ->willReturn('#fff'); $this->config ->expects($this->exactly(2)) @@ -534,7 +534,7 @@ class ThemingDefaultsTest extends TestCase { $this->config ->expects($this->once()) ->method('getUserValue') - ->with('user', 'theming', 'background', '') + ->with('user', 'theming', 'background_image', '') ->willReturn('nextcloud'); $this->config ->expects($this->exactly(3)) @@ -781,7 +781,7 @@ class ThemingDefaultsTest extends TestCase { ['logo', true, 'custom-logo?v=0'], ['logoheader', true, 'custom-logoheader?v=0'], ['favicon', true, 'custom-favicon?v=0'], - ['background', true, 'custom-background?v=0'], + ['background_image', true, 'custom-background?v=0'], ]); $expected = [ |