Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>tags/v26.0.0beta1
'url' => '/background/{type}', | 'url' => '/background/{type}', | ||||
'verb' => 'POST', | 'verb' => 'POST', | ||||
], | ], | ||||
[ | |||||
'name' => 'userTheme#deleteBackground', | |||||
'url' => '/background/custom', | |||||
'verb' => 'DELETE', | |||||
], | |||||
], | ], | ||||
'ocs' => [ | 'ocs' => [ | ||||
[ | [ |
#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 */ |
#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; | |||||
} | |||||
} |
'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'disable-user-theming' | 'name', 'url', 'imprintUrl', 'privacyUrl', 'slogan', 'color', 'disable-user-theming' | ||||
]; | ]; | ||||
public const SUPPORTED_IMAGE_KEYS = [ | |||||
'background', 'logo', 'favicon', 'logoheader' | |||||
]; | |||||
private $themingDefaults; | private $themingDefaults; | ||||
private $imageManager; | private $imageManager; | ||||
private $config; | private $config; | ||||
$value = $this->config->getAppValue('theming', $key, ''); | $value = $this->config->getAppValue('theming', $key, ''); | ||||
$output->writeln('- ' . $key . ': ' . $value . ''); | $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', ''); | $value = $this->config->getAppValue('theming', $key . 'Mime', ''); | ||||
$output->writeln('- ' . $key . ': ' . $value . ''); | $output->writeln('- ' . $key . ': ' . $value . ''); | ||||
} | } | ||||
return 0; | 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>'); | $output->writeln('<error>Invalid config key provided</error>'); | ||||
return 1; | return 1; | ||||
} | } | ||||
return 0; | return 0; | ||||
} | } | ||||
if (in_array($key, self::SUPPORTED_IMAGE_KEYS, true)) { | |||||
if (in_array($key, ImageManager::SUPPORTED_IMAGE_KEYS, true)) { | |||||
if (strpos($value, '/') !== 0) { | if (strpos($value, '/') !== 0) { | ||||
$output->writeln('<error>The image file needs to be provided as an absolute path: ' . $value . '.</error>'); | $output->writeln('<error>The image file needs to be provided as an absolute path: ' . $value . '.</error>'); | ||||
return 1; | return 1; |
/** | /** | ||||
* @NoAdminRequired | * @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'); | $currentVersion = (int)$this->config->getUserValue($this->userId, Application::APP_ID, 'userCacheBuster', '0'); | ||||
try { | try { | ||||
switch ($type) { | switch ($type) { | ||||
case 'shipped': | |||||
case BackgroundService::BACKGROUND_SHIPPED: | |||||
$this->backgroundService->setShippedBackground($value); | $this->backgroundService->setShippedBackground($value); | ||||
break; | break; | ||||
case 'custom': | |||||
case BackgroundService::BACKGROUND_CUSTOM: | |||||
$this->backgroundService->setFileBackground($value); | $this->backgroundService->setFileBackground($value); | ||||
break; | break; | ||||
case 'color': | case 'color': | ||||
$this->backgroundService->setColorBackground($value); | $this->backgroundService->setColorBackground($value); | ||||
break; | break; | ||||
case 'default': | |||||
case BackgroundService::BACKGROUND_DEFAULT: | |||||
$this->backgroundService->setDefaultBackground(); | $this->backgroundService->setDefaultBackground(); | ||||
break; | break; | ||||
default: | default: | ||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'userCacheBuster', (string)$currentVersion); | $this->config->setUserValue($this->userId, Application::APP_ID, 'userCacheBuster', (string)$currentVersion); | ||||
return new JSONResponse([ | 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, | 'version' => $currentVersion, | ||||
]); | ]); | ||||
} | } |
*/ | */ | ||||
namespace OCA\Theming; | namespace OCA\Theming; | ||||
use OCA\Theming\AppInfo\Application; | |||||
use OCA\Theming\Service\BackgroundService; | |||||
use OCP\Files\IAppData; | use OCP\Files\IAppData; | ||||
use OCP\Files\NotFoundException; | use OCP\Files\NotFoundException; | ||||
use OCP\Files\NotPermittedException; | use OCP\Files\NotPermittedException; | ||||
use OCP\IURLGenerator; | use OCP\IURLGenerator; | ||||
class ImageManager { | class ImageManager { | ||||
public const SupportedImageKeys = ['background', 'logo', 'logoheader', 'favicon']; | |||||
public const SUPPORTED_IMAGE_KEYS = ['background', 'logo', 'logoheader', 'favicon']; | |||||
/** @var IConfig */ | /** @var IConfig */ | ||||
private $config; | private $config; | ||||
$this->appData = $appData; | $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)) { | if ($this->hasImage($key)) { | ||||
return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter; | return $this->urlGenerator->linkToRoute('theming.Theming.getImage', [ 'key' => $key ]) . '?v=' . $cacheBusterCounter; | ||||
} | } | ||||
case 'favicon': | case 'favicon': | ||||
return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter; | return $this->urlGenerator->imagePath('core', 'logo/logo.png') . '?v=' . $cacheBusterCounter; | ||||
case 'background': | case 'background': | ||||
return $this->urlGenerator->imagePath('core', 'background.png') . '?v=' . $cacheBusterCounter; | |||||
return $this->urlGenerator->linkTo(Application::APP_ID, "img/background/" . BackgroundService::DEFAULT_BACKGROUND); | |||||
} | } | ||||
return ''; | 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)); | |||||
} | } | ||||
/** | /** | ||||
return $mimeSetting !== ''; | return $mimeSetting !== ''; | ||||
} | } | ||||
/** | |||||
* @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 | * Get folder for current theming files | ||||
* | * |
namespace OCA\Theming\Jobs; | namespace OCA\Theming\Jobs; | ||||
use OCA\Theming\AppInfo\Application; | use OCA\Theming\AppInfo\Application; | ||||
use OCP\App\IAppManager; | |||||
use OCP\AppFramework\Utility\ITimeFactory; | use OCP\AppFramework\Utility\ITimeFactory; | ||||
use OCP\BackgroundJob\IJobList; | use OCP\BackgroundJob\IJobList; | ||||
use OCP\BackgroundJob\QueuedJob; | use OCP\BackgroundJob\QueuedJob; |
if (!empty($user)) { | if (!empty($user)) { | ||||
$userId = $user->getUID(); | $userId = $user->getUID(); | ||||
/** User background */ | |||||
$this->initialState->provideInitialState( | $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( | $this->initialState->provideInitialState( | ||||
'themingDefaultBackground', | 'themingDefaultBackground', | ||||
$this->config->getAppValue('theming', 'backgroundMime', ''), | $this->config->getAppValue('theming', 'backgroundMime', ''), | ||||
); | ); | ||||
$this->initialState->provideInitialState( | |||||
'defaultShippedBackground', | |||||
BackgroundService::DEFAULT_BACKGROUND, | |||||
); | |||||
/** List of all shipped backgrounds */ | |||||
$this->initialState->provideInitialState( | $this->initialState->provideInitialState( | ||||
'shippedBackgrounds', | 'shippedBackgrounds', | ||||
BackgroundService::SHIPPED_BACKGROUNDS, | BackgroundService::SHIPPED_BACKGROUNDS, |
public const DEFAULT_COLOR = '#0082c9'; | public const DEFAULT_COLOR = '#0082c9'; | ||||
public const DEFAULT_ACCESSIBLE_COLOR = '#006aa3'; | 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 = [ | public const SHIPPED_BACKGROUNDS = [ | ||||
'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [ | 'anatoly-mikhaltsov-butterfly-wing-scale.jpg' => [ | ||||
'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)', | 'attribution' => 'Butterfly wing scale (Anatoly Mikhaltsov, CC BY-SA)', | ||||
} | } | ||||
public function setDefaultBackground(): void { | public function setDefaultBackground(): void { | ||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background'); | |||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'background_image'); | |||||
} | } | ||||
/** | /** | ||||
* @throws NoUserException | * @throws NoUserException | ||||
*/ | */ | ||||
public function setFileBackground($path): void { | 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); | $userFolder = $this->rootFolder->getUserFolder($this->userId); | ||||
/** @var File $file */ | /** @var File $file */ | ||||
if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) { | if (!array_key_exists($fileName, self::SHIPPED_BACKGROUNDS)) { | ||||
throw new InvalidArgumentException('The given file name is invalid'); | 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 { | public function setColorBackground(string $color): void { | ||||
if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) { | if (!preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) { | ||||
throw new InvalidArgumentException('The given color is invalid'); | 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 { | 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 { | try { | ||||
return $this->getAppDataFolder()->getFile('background.jpg'); | return $this->getAppDataFolder()->getFile('background.jpg'); | ||||
} catch (NotFoundException | NotPermittedException $e) { | } 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; | return null; |
$variables = []; | $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 | // If primary as background has been request or if we have a custom primary colour | ||||
// let's not define the background image | // let's not define the background image | ||||
if ($backgroundDeleted) { | if ($backgroundDeleted) { | ||||
} | } | ||||
// Register image variables only if custom-defined | // Register image variables only if custom-defined | ||||
foreach (ImageManager::SupportedImageKeys as $image) { | |||||
foreach (ImageManager::SUPPORTED_IMAGE_KEYS as $image) { | |||||
if ($this->imageManager->hasImage($image)) { | if ($this->imageManager->hasImage($image)) { | ||||
$imageUrl = $this->imageManager->getImageUrl($image); | $imageUrl = $this->imageManager->getImageUrl($image); | ||||
if ($image === 'background') { | if ($image === 'background') { | ||||
$variables['--image-background-size'] = 'cover'; | $variables['--image-background-size'] = 'cover'; | ||||
$variables['--image-background-default'] = "url('" . $imageUrl . "')"; | $variables['--image-background-default'] = "url('" . $imageUrl . "')"; | ||||
} | } | ||||
// --image-background is overriden by user theming | |||||
$variables["--image-$image"] = "url('" . $imageUrl . "')"; | $variables["--image-$image"] = "url('" . $imageUrl . "')"; | ||||
} | } | ||||
} | } | ||||
if ($user !== null | if ($user !== null | ||||
&& !$this->themingDefaults->isUserThemingDisabled() | && !$this->themingDefaults->isUserThemingDisabled() | ||||
&& $this->appManager->isEnabledForUser(Application::APP_ID)) { | && $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'); | $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 [ | 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 [ | 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(), | '--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 [ | return [ | ||||
'--image-background' => 'no', | |||||
'--image-background' => "url('" . $this->urlGenerator->linkTo(Application::APP_ID, "img/background/$backgroundImage") . "')", | |||||
'--color-background-plain' => $this->themingDefaults->getColorPrimary(), | '--color-background-plain' => $this->themingDefaults->getColorPrimary(), | ||||
'--background-image-invert-if-bright' => BackgroundService::SHIPPED_BACKGROUNDS[$backgroundImage]['theming'] ?? null === BackgroundService::THEMING_MODE_DARK ? 'invert(100%)' : 'no', | |||||
]; | ]; | ||||
} | } | ||||
} | } |
'--background-invert-if-dark' => 'no', | '--background-invert-if-dark' => 'no', | ||||
'--background-invert-if-bright' => 'invert(100%)', | '--background-invert-if-bright' => 'invert(100%)', | ||||
'--background-image-invert-if-bright' => 'no', | '--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 | // Primary variables |
} | } | ||||
// user-defined primary color | // user-defined primary color | ||||
$themingBackground = ''; | |||||
if (!empty($user)) { | 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 the user selected a specific colour | ||||
if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackground)) { | if (preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $themingBackground)) { | ||||
return $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 | // If the default color is not valid, return the default background one | ||||
$returnValue = $this->getSlogan(); | $returnValue = $this->getSlogan(); | ||||
break; | break; | ||||
case 'color': | case 'color': | ||||
$returnValue = $this->getColorPrimary(); | |||||
$returnValue = $this->getDefaultColorPrimary(); | |||||
break; | break; | ||||
case 'logo': | case 'logo': | ||||
case 'logoheader': | case 'logoheader': |
</template> | </template> | ||||
<template v-else> | <template v-else> | ||||
<p>{{ t('theming', 'Set a custom background') }}</p> | <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> | </template> | ||||
</NcSettingsSection> | </NcSettingsSection> | ||||
</section> | </section> | ||||
const enforceTheme = loadState('theming', 'enforceTheme', '') | const enforceTheme = loadState('theming', 'enforceTheme', '') | ||||
const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false) | const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false) | ||||
const background = loadState('theming', 'background') | |||||
const themingDefaultBackground = loadState('theming', 'themingDefaultBackground') | |||||
const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled') | const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled') | ||||
console.debug('Available themes', availableThemes) | console.debug('Available themes', availableThemes) | ||||
data() { | data() { | ||||
return { | return { | ||||
availableThemes, | availableThemes, | ||||
// Admin defined configs | |||||
enforceTheme, | enforceTheme, | ||||
shortcutsDisabled, | shortcutsDisabled, | ||||
background, | |||||
themingDefaultBackground, | |||||
isUserThemingDisabled, | isUserThemingDisabled, | ||||
} | } | ||||
}, | }, | ||||
}, | }, | ||||
methods: { | 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) { | updateBackground(data) { | ||||
this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value | this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value | ||||
this.$emit('update:background') | |||||
this.refreshGlobalStyles() | |||||
}, | }, | ||||
changeTheme({ enabled, id }) { | changeTheme({ enabled, id }) { |
<template> | <template> | ||||
<div class="background-selector"> | <div class="background-selector"> | ||||
<!-- Custom background --> | <!-- Custom background --> | ||||
<button class="background filepicker" | |||||
:class="{ active: background === 'custom' }" | |||||
<button class="background background__filepicker" | |||||
:class="{ 'background--active': backgroundImage === 'custom' }" | |||||
tabindex="0" | tabindex="0" | ||||
@click="pickFile"> | @click="pickFile"> | ||||
{{ t('theming', 'Pick from Files') }} | |||||
{{ t('theming', 'Custom background') }} | |||||
</button> | </button> | ||||
<!-- Default background --> | <!-- 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" | tabindex="0" | ||||
:class="{ 'icon-loading': loading === 'default', active: background === 'default' }" | |||||
@click="setDefault"> | @click="setDefault"> | ||||
{{ t('theming', 'Default image') }} | |||||
{{ t('theming', 'Default background') }} | |||||
<Check :size="44" /> | |||||
</button> | </button> | ||||
<!-- Custom color picker --> | <!-- Custom color picker --> | ||||
<NcColorPicker v-model="Theming.color" @input="debouncePickColor"> | <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="Theming.color" | ||||
:data-color-bright="invertTextColor(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> | </button> | ||||
</NcColorPicker> | </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 --> | <!-- Background set selection --> | ||||
<button v-for="shippedBackground in shippedBackgrounds" | <button v-for="shippedBackground in shippedBackgrounds" | ||||
:key="shippedBackground.name" | :key="shippedBackground.name" | ||||
v-tooltip="shippedBackground.details.attribution" | 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'" | :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> | </div> | ||||
</template> | </template> | ||||
<script> | <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 { loadState } from '@nextcloud/initial-state' | ||||
import { prefixWithBaseUrl } from '../helpers/prefixWithBaseUrl.js' | |||||
import axios from '@nextcloud/axios' | 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 debounce from 'debounce' | ||||
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker' | import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker' | ||||
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' | import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip' | ||||
const backgroundColor = loadState('theming', 'backgroundColor') | |||||
const backgroundImage = loadState('theming', 'backgroundImage') | |||||
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds') | 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 { | export default { | ||||
name: 'BackgroundSettings', | name: 'BackgroundSettings', | ||||
}, | }, | ||||
components: { | components: { | ||||
Check, | |||||
Close, | |||||
NcColorPicker, | NcColorPicker, | ||||
}, | }, | ||||
props: { | |||||
background: { | |||||
type: String, | |||||
default: 'default', | |||||
}, | |||||
themingDefaultBackground: { | |||||
type: String, | |||||
default: '', | |||||
}, | |||||
}, | |||||
data() { | data() { | ||||
return { | return { | ||||
backgroundImage: generateUrl('/apps/theming/background') + '?v=' + Date.now(), | |||||
loading: false, | loading: false, | ||||
Theming: loadState('theming', 'data', {}), | Theming: loadState('theming', 'data', {}), | ||||
// User background image and color settings | |||||
backgroundImage, | |||||
backgroundColor, | |||||
} | } | ||||
}, | }, | ||||
computed: { | computed: { | ||||
shippedBackgrounds() { | 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' | |||||
}, | }, | ||||
}, | }, | ||||
: null | : 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) { | 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() { | async setDefault() { | ||||
this.update(result.data) | 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) { | async pickColor(event) { | ||||
this.loading = 'color' | this.loading = 'color' | ||||
const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9' | const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9' | ||||
const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color }) | const result = await axios.post(generateUrl('/apps/theming/background/color'), { value: color }) | ||||
this.update(result.data) | this.update(result.data) | ||||
}, | }, | ||||
debouncePickColor: debounce(function() { | |||||
this.pickColor(...arguments) | |||||
}, 200), | |||||
pickFile() { | pickFile() { | ||||
window.OC.dialogs.filepicker(t('theming', 'Select a background from your files'), (path, type) => { | window.OC.dialogs.filepicker(t('theming', 'Select a background from your files'), (path, type) => { | ||||
justify-content: center; | justify-content: center; | ||||
.background { | .background { | ||||
overflow: hidden; | |||||
width: 176px; | width: 176px; | ||||
height: 96px; | height: 96px; | ||||
margin: 8px; | margin: 8px; | ||||
background-size: cover; | |||||
background-position: center center; | |||||
text-align: center; | text-align: center; | ||||
border-radius: var(--border-radius-large); | |||||
border: 2px solid var(--color-main-background); | 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); | border-color: var(--color-border); | ||||
} | } | ||||
&.color { | |||||
background-color: var(--color-primary-default); | |||||
&__color { | |||||
color: var(--color-primary-text); | 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, | &:hover, | ||||
&:focus { | &: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> | </style> |
/** | |||||
* @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) | |||||
} |
/** | |||||
* @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 |
public function testGetLoginBackground() { | public function testGetLoginBackground() { | ||||
$file = $this->createMock(ISimpleFile::class); | $file = $this->createMock(ISimpleFile::class); | ||||
$file->method('getName')->willReturn('background.png'); | |||||
$file->method('getName')->willReturn('app-background.jpg'); | |||||
$file->method('getMTime')->willReturn(42); | $file->method('getMTime')->willReturn(42); | ||||
$this->imageManager->expects($this->once()) | $this->imageManager->expects($this->once()) | ||||
->method('getImage') | ->method('getImage') |
$this->config | $this->config | ||||
->expects($this->once()) | ->expects($this->once()) | ||||
->method('getUserValue') | ->method('getUserValue') | ||||
->with('user', 'theming', 'background', '') | |||||
->with('user', 'theming', 'background_image', '') | |||||
->willReturn(array_keys(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]); | ->willReturn(array_keys(BackgroundService::SHIPPED_BACKGROUNDS)[$backgroundIndex]); | ||||
$this->config | $this->config | ||||
->expects($this->exactly(2)) | ->expects($this->exactly(2)) | ||||
$this->config | $this->config | ||||
->expects($this->once()) | ->expects($this->once()) | ||||
->method('getUserValue') | ->method('getUserValue') | ||||
->with('user', 'theming', 'background', '') | |||||
->with('user', 'theming', 'background_image', '') | |||||
->willReturn('#fff'); | ->willReturn('#fff'); | ||||
$this->config | $this->config | ||||
->expects($this->exactly(2)) | ->expects($this->exactly(2)) | ||||
$this->config | $this->config | ||||
->expects($this->once()) | ->expects($this->once()) | ||||
->method('getUserValue') | ->method('getUserValue') | ||||
->with('user', 'theming', 'background', '') | |||||
->with('user', 'theming', 'background_image', '') | |||||
->willReturn('nextcloud'); | ->willReturn('nextcloud'); | ||||
$this->config | $this->config | ||||
->expects($this->exactly(3)) | ->expects($this->exactly(3)) | ||||
['logo', true, 'custom-logo?v=0'], | ['logo', true, 'custom-logo?v=0'], | ||||
['logoheader', true, 'custom-logoheader?v=0'], | ['logoheader', true, 'custom-logoheader?v=0'], | ||||
['favicon', true, 'custom-favicon?v=0'], | ['favicon', true, 'custom-favicon?v=0'], | ||||
['background', true, 'custom-background?v=0'], | |||||
['background_image', true, 'custom-background?v=0'], | |||||
]); | ]); | ||||
$expected = [ | $expected = [ |
body { | body { | ||||
background-color: var(--color-background-plain, var(--color-main-background)); | background-color: var(--color-background-plain, var(--color-main-background)); | ||||
background-image: var(--image-background-plain, var(--image-background)); | |||||
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default))); | |||||
background-size: cover; | background-size: cover; | ||||
background-position: center; | background-position: center; | ||||
position: fixed; | position: fixed; |
body { | body { | ||||
background-color: var(--color-background-plain, var(--color-main-background)); | background-color: var(--color-background-plain, var(--color-main-background)); | ||||
background-image: var(--image-background-plain, var(--image-background)); | |||||
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default))); | |||||
background-size: cover; | background-size: cover; | ||||
background-position: center; | background-position: center; | ||||
position: fixed; | position: fixed; |
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; | ||||
color: var(--color-text); | color: var(--color-text); | ||||
text-align: center; | text-align: center; | ||||
background-color: var(--color-primary-default, var(--color-primary)); | |||||
background-image: var(--image-background-plain, var(--image-background, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))); | |||||
background-color: var(--color-main-background-not-plain, var(--color-primary)); | |||||
background-image: var(--image-background, var(--image-background-plain, linear-gradient(40deg, #0082c9 0%, #30b6ff 100%))); | |||||
background-attachment: fixed; | background-attachment: fixed; | ||||
min-height: 100%; /* fix sticky footer */ | min-height: 100%; /* fix sticky footer */ | ||||
height: auto; | height: auto; |
body { | body { | ||||
background-color: var(--color-background-plain, var(--color-main-background)); | background-color: var(--color-background-plain, var(--color-main-background)); | ||||
background-image: var(--image-background-plain, var(--image-background)); | |||||
background-image: var(--image-background-plain, var(--image-background, var(--image-background-default))); | |||||
background-size: cover; | background-size: cover; | ||||
background-position: center; | background-position: center; | ||||
position: fixed; | position: fixed; |