Rewrite admin theming in Vue

Signed-off-by: Christopher Ng <chrng8@gmail.com>
This commit is contained in:
Christopher Ng 2022-10-01 03:04:39 +00:00
parent d007088cf5
commit 4a2bbc7af9
33 changed files with 1191 additions and 246 deletions

View File

@ -55,6 +55,7 @@
--background-invert-if-bright: invert(100%);
--background-image-invert-if-bright: no;
--image-background: url('/core/img/app-background.jpg');
--image-background-default: url('/core/img/app-background.jpg');
--color-background-plain: #0082c9;
--primary-invert-if-bright: no;
--color-primary: #00639a;
@ -66,6 +67,7 @@
--color-primary-light-hover: #dbe4e9;
--color-primary-text-dark: #ededed;
--color-primary-element: #00639a;
--color-primary-element-default-hover: #329bd3;
--color-primary-element-text: #ffffff;
--color-primary-element-hover: #3282ae;
--color-primary-element-light: #e5eff4;

View File

@ -53,7 +53,6 @@ class ImageManager {
private $appData;
/** @var IURLGenerator */
private $urlGenerator;
/** @var array */
/** @var ICacheFactory */
private $cacheFactory;
/** @var ILogger */
@ -137,20 +136,6 @@ class ImageManager {
return $mimeSetting !== '';
}
/**
* @return array<string, array{mime: string, url: string}>
*/
public function getCustomImages(): array {
$images = [];
foreach ($this::SupportedImageKeys as $key) {
$images[$key] = [
'mime' => $this->config->getAppValue('theming', $key . 'Mime', ''),
'url' => $this->getImageUrl($key),
];
}
return $images;
}
/**
* Get folder for current theming files
*

View File

@ -27,19 +27,23 @@
*/
namespace OCA\Theming\Settings;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\ImageManager;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IDelegatedSettings;
use OCP\Util;
class Admin implements IDelegatedSettings {
private string $appName;
private IConfig $config;
private IL10N $l;
private ThemingDefaults $themingDefaults;
private IInitialState $initialState;
private IURLGenerator $urlGenerator;
private ImageManager $imageManager;
@ -47,12 +51,14 @@ class Admin implements IDelegatedSettings {
IConfig $config,
IL10N $l,
ThemingDefaults $themingDefaults,
IInitialState $initialState,
IURLGenerator $urlGenerator,
ImageManager $imageManager) {
$this->appName = $appName;
$this->config = $config;
$this->l = $l;
$this->themingDefaults = $themingDefaults;
$this->initialState = $initialState;
$this->urlGenerator = $urlGenerator;
$this->imageManager = $imageManager;
}
@ -69,23 +75,28 @@ class Admin implements IDelegatedSettings {
$errorMessage = $this->l->t('You are already using a custom theme. Theming app settings might be overwritten by that.');
}
$parameters = [
'themable' => $themable,
'errorMessage' => $errorMessage,
$this->initialState->provideInitialState('adminThemingParameters', [
'isThemable' => $themable,
'notThemableErrorMessage' => $errorMessage,
'name' => $this->themingDefaults->getEntity(),
'url' => $this->themingDefaults->getBaseUrl(),
'slogan' => $this->themingDefaults->getSlogan(),
'color' => $this->themingDefaults->getDefaultColorPrimary(),
'uploadLogoRoute' => $this->urlGenerator->linkToRoute('theming.Theming.uploadImage'),
'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''),
'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''),
'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''),
'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''),
'legalNoticeUrl' => $this->themingDefaults->getImprintUrl(),
'privacyPolicyUrl' => $this->themingDefaults->getPrivacyUrl(),
'docUrl' => $this->urlGenerator->linkToDocs('admin-theming'),
'docUrlIcons' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
'canThemeIcons' => $this->imageManager->shouldReplaceIcons(),
'iconDocs' => $this->urlGenerator->linkToDocs('admin-theming-icons'),
'images' => $this->imageManager->getCustomImages(),
'imprintUrl' => $this->themingDefaults->getImprintUrl(),
'privacyUrl' => $this->themingDefaults->getPrivacyUrl(),
'userThemingDisabled' => $this->themingDefaults->isUserThemingDisabled(),
];
]);
return new TemplateResponse($this->appName, 'settings-admin', $parameters, '');
Util::addScript($this->appName, 'admin-theming');
return new TemplateResponse($this->appName, 'settings-admin');
}
/**

View File

@ -77,7 +77,8 @@ class Personal implements ISettings {
$this->initialStateService->provideInitialState('themes', array_values($themes));
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
Util::addScript($this->appName, 'theming-settings');
Util::addScript($this->appName, 'personal-theming');
return new TemplateResponse($this->appName, 'settings-personal');
}

View File

@ -40,6 +40,7 @@ trait CommonThemeTrait {
protected function generatePrimaryVariables(string $colorMainBackground, string $colorMainText): array {
$colorPrimaryLight = $this->util->mix($this->primaryColor, $colorMainBackground, -80);
$colorPrimaryElement = $this->util->elementColor($this->primaryColor);
$colorPrimaryElementDefault = $this->util->elementColor($this->defaultPrimaryColor);
$colorPrimaryElementLight = $this->util->mix($colorPrimaryElement, $colorMainBackground, -80);
// primary related colours
@ -64,6 +65,7 @@ trait CommonThemeTrait {
// used for buttons, inputs...
'--color-primary-element' => $colorPrimaryElement,
'--color-primary-element-default-hover' => $this->util->mix($colorPrimaryElementDefault, $colorMainBackground, 60),
'--color-primary-element-text' => $this->util->invertTextColor($colorPrimaryElement) ? '#000000' : '#ffffff',
'--color-primary-element-hover' => $this->util->mix($colorPrimaryElement, $colorMainBackground, 60),
'--color-primary-element-light' => $colorPrimaryElementLight,
@ -80,6 +82,7 @@ trait CommonThemeTrait {
* Generate admin theming background-related variables
*/
protected function generateGlobalBackgroundVariables(): array {
$user = $this->userSession->getUser();
$backgroundDeleted = $this->config->getAppValue(Application::APP_ID, 'backgroundMime', '') === 'backgroundColor';
$hasCustomLogoHeader = $this->imageManager->hasImage('logo') || $this->imageManager->hasImage('logoheader');
@ -87,9 +90,11 @@ trait CommonThemeTrait {
// If primary as background has been request or if we have a custom primary colour
// let's not define the background image
if ($backgroundDeleted && $this->themingDefaults->isUserThemingDisabled()) {
$variables['--image-background-plain'] = 'true';
if ($backgroundDeleted) {
$variables['--color-background-plain'] = $this->themingDefaults->getColorPrimary();
if ($this->themingDefaults->isUserThemingDisabled() || $user === null) {
$variables['--image-background-plain'] = 'true';
}
}
// Register image variables only if custom-defined
@ -99,9 +104,11 @@ trait CommonThemeTrait {
if ($image === 'background') {
// If background deleted is set, ignoring variable
if ($backgroundDeleted) {
$variables['--image-background-default'] = 'no';
continue;
}
$variables['--image-background-size'] = 'cover';
$variables['--image-background-default'] = "url('" . $imageUrl . "')";
}
$variables["--image-$image"] = "url('" . $imageUrl . "')";
}

View File

@ -193,6 +193,7 @@ class DefaultTheme implements ITheme {
// 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,
];

View File

@ -224,7 +224,7 @@ class ThemingDefaults extends \OC_Defaults {
if ($this->isUserThemingDisabled()) {
return $defaultColor;
}
// user-defined primary color
$themingBackground = '';
if (!empty($user)) {

View File

@ -0,0 +1,303 @@
<!--
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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/>.
-
-->
<template>
<section>
<NcSettingsSection :title="t('theming', 'Theming')"
:description="t('theming', 'Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')"
:doc-url="docUrl">
<div class="admin-theming">
<NcNoteCard v-if="!isThemable"
type="error"
:show-alert="true">
<p>{{ notThemableErrorMessage }}</p>
</NcNoteCard>
<TextField v-for="field in textFields"
:key="field.name"
:name="field.name"
:value.sync="field.value"
:default-value="field.defaultValue"
:type="field.type"
:display-name="field.displayName"
:placeholder="field.placeholder"
:maxlength="field.maxlength"
@update:theming="$emit('update:theming')" />
<ColorPickerField :name="colorPickerField.name"
:value.sync="colorPickerField.value"
:default-value="colorPickerField.defaultValue"
:display-name="colorPickerField.displayName"
@update:theming="$emit('update:theming')" />
<FileInputField v-for="field in fileInputFields"
:key="field.name"
:name="field.name"
:mime-name="field.mimeName"
:mime-value.sync="field.mimeValue"
:default-mime-value="field.defaultMimeValue"
:display-name="field.displayName"
:aria-label="field.ariaLabel"
@update:theming="$emit('update:theming')" />
<div class="admin-theming__preview">
<div class="admin-theming__preview-logo" />
</div>
</div>
</NcSettingsSection>
<NcSettingsSection :title="t('theming', 'Advanced options')">
<div class="admin-theming-advanced">
<TextField v-for="field in advancedTextFields"
:key="field.name"
:name="field.name"
:value.sync="field.value"
:default-value="field.defaultValue"
:type="field.type"
:display-name="field.displayName"
:placeholder="field.placeholder"
:maxlength="field.maxlength"
@update:theming="$emit('update:theming')" />
<FileInputField v-for="field in advancedFileInputFields"
:key="field.name"
:name="field.name"
:mime-name="field.mimeName"
:mime-value.sync="field.mimeValue"
:default-mime-value="field.defaultMimeValue"
:display-name="field.displayName"
:aria-label="field.ariaLabel"
@update:theming="$emit('update:theming')" />
<CheckboxField :name="userThemingField.name"
:value="userThemingField.value"
:default-value="userThemingField.defaultValue"
:display-name="userThemingField.displayName"
:label="userThemingField.label"
:description="userThemingField.description"
@update:theming="$emit('update:theming')" />
<a v-if="!canThemeIcons"
:href="docUrlIcons"
rel="noreferrer noopener">
<em>{{ t('theming', 'Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.') }}</em>
</a>
</div>
</NcSettingsSection>
</section>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import {
NcNoteCard,
NcSettingsSection,
} from '@nextcloud/vue'
import CheckboxField from './components/admin/CheckboxField.vue'
import ColorPickerField from './components/admin/ColorPickerField.vue'
import FileInputField from './components/admin/FileInputField.vue'
import TextField from './components/admin/TextField.vue'
const {
backgroundMime,
canThemeIcons,
color,
docUrl,
docUrlIcons,
faviconMime,
isThemable,
legalNoticeUrl,
logoheaderMime,
logoMime,
name,
notThemableErrorMessage,
privacyPolicyUrl,
slogan,
url,
userThemingDisabled,
} = loadState('theming', 'adminThemingParameters')
const textFields = [
{
name: 'name',
value: name,
defaultValue: 'Nextcloud',
type: 'text',
displayName: t('theming', 'Name'),
placeholder: t('theming', 'Name'),
maxlength: 250,
},
{
name: 'url',
value: url,
defaultValue: 'https://nextcloud.com',
type: 'url',
displayName: t('theming', 'Web link'),
placeholder: 'https://…',
maxlength: 500,
},
{
name: 'slogan',
value: slogan,
defaultValue: t('theming', 'a safe home for all your data'),
type: 'text',
displayName: t('theming', 'Slogan'),
placeholder: t('theming', 'Slogan'),
maxlength: 500,
},
]
const colorPickerField = {
name: 'color',
value: color,
defaultValue: '#0082c9',
displayName: t('theming', 'Color'),
}
const fileInputFields = [
{
name: 'logo',
mimeName: 'logoMime',
mimeValue: logoMime,
defaultMimeValue: '',
displayName: t('theming', 'Logo'),
ariaLabel: t('theming', 'Upload new logo'),
},
{
name: 'background',
mimeName: 'backgroundMime',
mimeValue: backgroundMime,
defaultMimeValue: '',
displayName: t('theming', 'Background and login image'),
ariaLabel: t('theming', 'Upload new background and login image'),
},
]
const advancedTextFields = [
{
name: 'imprintUrl',
value: legalNoticeUrl,
defaultValue: '',
type: 'url',
displayName: t('theming', 'Legal notice link'),
placeholder: 'https://…',
maxlength: 500,
},
{
name: 'privacyUrl',
value: privacyPolicyUrl,
defaultValue: '',
type: 'url',
displayName: t('theming', 'Privacy policy link'),
placeholder: 'https://…',
maxlength: 500,
},
]
const advancedFileInputFields = [
{
name: 'logoheader',
mimeName: 'logoheaderMime',
mimeValue: logoheaderMime,
defaultMimeValue: '',
displayName: t('theming', 'Header logo'),
ariaLabel: t('theming', 'Upload new header logo'),
},
{
name: 'favicon',
mimeName: 'faviconMime',
mimeValue: faviconMime,
defaultMimeValue: '',
displayName: t('theming', 'Favicon'),
ariaLabel: t('theming', 'Upload new favicon'),
},
]
const userThemingField = {
name: 'disable-user-theming',
value: userThemingDisabled,
defaultValue: false,
displayName: t('theming', 'User settings'),
label: t('theming', 'Disable user theming'),
description: t('theming', 'Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on.'),
}
export default {
name: 'AdminTheming',
components: {
CheckboxField,
ColorPickerField,
FileInputField,
NcNoteCard,
NcSettingsSection,
TextField,
},
emits: [
'update:theming',
],
data() {
return {
textFields,
colorPickerField,
fileInputFields,
advancedTextFields,
advancedFileInputFields,
userThemingField,
canThemeIcons,
docUrl,
docUrlIcons,
isThemable,
notThemableErrorMessage,
}
},
}
</script>
<style lang="scss" scoped>
.admin-theming,
.admin-theming-advanced {
display: flex;
flex-direction: column;
gap: 8px 0;
}
.admin-theming {
&__preview {
width: 230px;
height: 140px;
background-size: cover;
background-position: center;
text-align: center;
margin-top: 10px;
background-color: var(--color-primary-default);
background-image: var(--image-background-default, var(--image-background-plain, url('../../../core/img/app-background.jpg'), linear-gradient(40deg, #0082c9 0%, #30b6ff 100%)));
&-logo {
width: 20%;
height: 20%;
margin-top: 20px;
display: inline-block;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
background-image: var(--image-logo, url('../../../core/img/logo/logo.svg'));
}
}
}
</style>

View File

@ -0,0 +1,33 @@
/**
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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 Vue from 'vue'
import App from './AdminTheming.vue'
import { refreshStyles } from './helpers/refreshStyles.js'
Vue.prototype.OC = OC
Vue.prototype.t = t
const View = Vue.extend(App)
const theming = new View()
theming.$mount('#admin-theming')
theming.$on('update:theming', refreshStyles)

View File

@ -0,0 +1,102 @@
<!--
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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/>.
-
-->
<template>
<div class="field">
<label :for="id">{{ displayName }}</label>
<div class="field__row">
<NcCheckboxRadioSwitch type="switch"
:id="id"
:checked.sync="localValue"
@update:checked="save">
{{ label }}
</NcCheckboxRadioSwitch>
</div>
<p class="field__description">{{ description }}</p>
<NcNoteCard v-if="errorMessage"
type="error"
:show-alert="true">
<p>{{ errorMessage }}</p>
</NcNoteCard>
</div>
</template>
<script>
import {
NcCheckboxRadioSwitch,
NcNoteCard,
} from '@nextcloud/vue'
import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
export default {
name: 'CheckboxField',
components: {
NcCheckboxRadioSwitch,
NcNoteCard,
},
mixins: [
TextValueMixin,
],
props: {
name: {
type: String,
required: true,
},
value: {
type: Boolean,
required: true,
},
defaultValue: {
type: Boolean,
required: true,
},
displayName: {
type: String,
required: true,
},
label: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
},
}
</script>
<style lang="scss" scoped>
@import './shared/field.scss';
.field {
&__description {
color: var(--color-text-maxcontrast);
}
}
</style>

View File

@ -0,0 +1,121 @@
<!--
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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/>.
-
-->
<template>
<div class="field">
<label :for="id">{{ displayName }}</label>
<div class="field__row">
<NcColorPicker :value.sync="localValue"
:advanced-fields="true"
@update:value="debounceSave">
<NcButton class="field__button"
type="primary"
:id="id"
:aria-label="t('theming', 'Select a custom color')">
{{ value }}
</NcButton>
</NcColorPicker>
<NcButton v-if="value !== defaultValue"
type="tertiary"
:aria-label="t('theming', 'Reset to default')"
@click="undo">
<template #icon>
<Undo :size="20" />
</template>
</NcButton>
</div>
<NcNoteCard v-if="errorMessage"
type="error"
:show-alert="true">
<p>{{ errorMessage }}</p>
</NcNoteCard>
</div>
</template>
<script>
import { debounce } from 'debounce'
import {
NcButton,
NcColorPicker,
NcNoteCard,
} from '@nextcloud/vue'
import Undo from 'vue-material-design-icons/UndoVariant.vue'
import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
export default {
name: 'ColorPickerField',
components: {
NcButton,
NcColorPicker,
NcNoteCard,
Undo,
},
mixins: [
TextValueMixin,
],
props: {
name: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
defaultValue: {
type: String,
required: true,
},
displayName: {
type: String,
required: true,
},
},
methods: {
debounceSave: debounce(async function() {
await this.save()
}, 200),
},
}
</script>
<style lang="scss" scoped>
@import './shared/field.scss';
.field {
// Override default NcButton styles
&__button {
width: 230px !important;
border-radius: var(--border-radius-large) !important;
background-color: var(--color-primary-default) !important;
&:hover {
background-color: var(--color-primary-element-default-hover) !important;
}
}
}
</style>

View File

@ -0,0 +1,248 @@
<!--
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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/>.
-
-->
<template>
<div class="field">
<label :for="id">{{ displayName }}</label>
<div class="field__row">
<NcButton type="secondary"
:id="id"
:aria-label="ariaLabel"
@click="activateLocalFilePicker">
<template #icon>
<Upload :size="20" />
</template>
{{ t('theming', 'Upload') }}
</NcButton>
<NcButton v-if="showReset"
type="tertiary"
:aria-label="t('theming', 'Reset to default')"
@click="undo">
<template #icon>
<Undo :size="20" />
</template>
</NcButton>
<NcButton v-if="showRemove"
type="tertiary"
:aria-label="t('theming', 'Remove background image')"
@click="removeBackground">
<template #icon>
<Delete :size="20" />
</template>
</NcButton>
<NcLoadingIcon v-if="showLoading"
class="field__loading-icon"
:size="20" />
</div>
<div v-if="(name === 'logoheader' || name === 'favicon') && mimeValue !== defaultMimeValue"
class="field__preview"
:class="{
'field__preview--logoheader': name === 'logoheader',
'field__preview--favicon': name === 'favicon',
}" />
<NcNoteCard v-if="errorMessage"
type="error"
:show-alert="true">
<p>{{ errorMessage }}</p>
</NcNoteCard>
<input ref="input"
type="file"
@change="onChange">
</div>
</template>
<script>
import axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import {
NcButton,
NcLoadingIcon,
NcNoteCard,
} from '@nextcloud/vue'
import Delete from 'vue-material-design-icons/Delete.vue'
import Undo from 'vue-material-design-icons/UndoVariant.vue'
import Upload from 'vue-material-design-icons/Upload.vue'
import FieldMixin from '../../mixins/admin/FieldMixin.js'
export default {
name: 'FileInputField',
components: {
Delete,
NcButton,
NcLoadingIcon,
NcNoteCard,
Undo,
Upload,
},
mixins: [
FieldMixin,
],
props: {
name: {
type: String,
required: true,
},
mimeName: {
type: String,
required: true,
},
mimeValue: {
type: String,
required: true,
},
defaultMimeValue: {
type: String,
required: true,
},
displayName: {
type: String,
required: true,
},
ariaLabel: {
type: String,
required: true,
},
},
data() {
return {
showLoading: false,
}
},
computed: {
showReset() {
return this.mimeValue !== this.defaultMimeValue
},
showRemove() {
if (this.name === 'background') {
if (this.mimeValue.startsWith('image/')) {
return true
}
if (this.mimeValue === this.defaultMimeValue) {
return true
}
}
return false
},
},
methods: {
activateLocalFilePicker() {
this.reset()
// Set to null so that selecting the same file will trigger the change event
this.$refs.input.value = null
this.$refs.input.click()
},
async onChange(e) {
const file = e.target.files[0]
const formData = new FormData()
formData.append('key', this.name)
formData.append('image', file)
const url = generateUrl('/apps/theming/ajax/uploadImage')
try {
this.showLoading = true
await axios.post(url, formData)
this.showLoading = false
this.$emit('update:mime-value', file.type)
this.handleSuccess()
} catch (e) {
this.showLoading = false
this.errorMessage = e.response.data.data?.message
}
},
async undo() {
this.reset()
const url = generateUrl('/apps/theming/ajax/undoChanges')
try {
await axios.post(url, {
setting: this.mimeName,
})
this.$emit('update:mime-value', this.defaultMimeValue)
this.handleSuccess()
} catch (e) {
this.errorMessage = e.response.data.data?.message
}
},
async removeBackground() {
this.reset()
const url = generateUrl('/apps/theming/ajax/updateStylesheet')
try {
await axios.post(url, {
setting: this.mimeName,
value: 'backgroundColor',
})
this.$emit('update:mime-value', 'backgroundColor')
this.handleSuccess()
} catch (e) {
this.errorMessage = e.response.data.data?.message
}
},
},
}
</script>
<style lang="scss" scoped>
@import './shared/field.scss';
.field {
&__loading-icon {
width: 44px;
height: 44px;
}
&__preview {
width: 70px;
height: 70px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
margin: 10px 0;
&--logoheader {
background-image: var(--image-logoheader);
}
&--favicon {
background-image: var(--image-favicon);
}
}
}
input[type="file"] {
display: none;
}
</style>

View File

@ -0,0 +1,98 @@
<!--
- @copyright 2022 Christopher Ng <chrng8@gmail.com>
-
- @author Christopher Ng <chrng8@gmail.com>
-
- @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/>.
-
-->
<template>
<div class="field">
<!-- PENDING undo trailing button icon requires @nextcloud/vue release and bump -->
<!-- PENDING custom maxlength requires @nextcloud/vue release and bump -->
<NcTextField :value.sync="localValue"
:label="displayName"
:label-visible="true"
:placeholder="placeholder"
:type="type"
:maxlength="maxlength"
:spellcheck="false"
:success="showSuccess"
:error="Boolean(errorMessage)"
:helper-text="errorMessage"
:show-trailing-button="value !== defaultValue"
trailing-button-icon="undo"
@trailing-button-click="undo"
@keydown.enter="save"
@blur="save" />
</div>
</template>
<script>
import { NcTextField } from '@nextcloud/vue'
import TextValueMixin from '../../mixins/admin/TextValueMixin.js'
export default {
name: 'TextField',
components: {
NcTextField,
},
mixins: [
TextValueMixin,
],
props: {
name: {
type: String,
required: true,
},
value: {
type: String,
required: true,
},
defaultValue: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
displayName: {
type: String,
required: true,
},
placeholder: {
type: String,
required: true,
},
maxlength: {
type: Number,
required: true,
},
},
}
</script>
<style lang="scss" scoped>
.field {
max-width: 400px;
}
</style>

View File

@ -0,0 +1,32 @@
/**
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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/>.
*
*/
.field {
display: flex;
flex-direction: column;
gap: 4px 0;
&__row {
display: flex;
gap: 0 4px;
}
}

View File

@ -0,0 +1,33 @@
/**
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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/>.
*
*/
export const refreshStyles = () => {
// Refresh server-side generated theming CSS
[...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)
})
}

View File

@ -0,0 +1,64 @@
/**
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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/>.
*
*/
const styleRefreshFields = [
'color',
'logo',
'background',
'logoheader',
'favicon',
'disable-user-theming',
]
export default {
emits: [
'update:theming',
],
data() {
return {
showSuccess: false,
errorMessage: '',
}
},
computed: {
id() {
return `admin-theming-${this.name}`
},
},
methods: {
reset() {
this.showSuccess = false
this.errorMessage = ''
},
handleSuccess() {
this.showSuccess = true
setTimeout(() => { this.showSuccess = false }, 2000)
if (styleRefreshFields.includes(this.name)) {
this.$emit('update:theming')
}
},
},
}

View File

@ -0,0 +1,77 @@
/**
* @copyright 2022 Christopher Ng <chrng8@gmail.com>
*
* @author Christopher Ng <chrng8@gmail.com>
*
* @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 axios from '@nextcloud/axios'
import { generateUrl } from '@nextcloud/router'
import FieldMixin from './FieldMixin.js'
export default {
mixins: [
FieldMixin,
],
watch: {
value(value) {
this.localValue = value
},
},
data() {
return {
localValue: this.value,
}
},
methods: {
async save() {
this.reset()
const url = generateUrl('/apps/theming/ajax/updateStylesheet')
// Convert boolean to string as server expects string value
const valueToPost = this.localValue === true ? 'yes' : this.localValue === false ? 'no' : this.localValue
try {
await axios.post(url, {
setting: this.name,
value: valueToPost,
})
this.$emit('update:value', this.localValue)
this.handleSuccess()
} catch (e) {
this.errorMessage = e.response.data.data?.message
}
},
async undo() {
this.reset()
const url = generateUrl('/apps/theming/ajax/undoChanges')
try {
await axios.post(url, {
setting: this.name,
})
this.$emit('update:value', this.defaultValue)
this.handleSuccess()
} catch (e) {
this.errorMessage = e.response.data.data?.message
}
},
},
}

View File

@ -22,23 +22,12 @@
import Vue from 'vue'
import App from './UserThemes.vue'
import { refreshStyles } from './helpers/refreshStyles.js'
// bind to window
Vue.prototype.OC = OC
Vue.prototype.t = t
const View = Vue.extend(App)
const theming = new View()
theming.$mount('#theming')
theming.$on('update:background', () => {
// Refresh server-side generated theming CSS
[...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)
})
})
theming.$on('update:background', refreshStyles)

View File

@ -22,135 +22,6 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
script('theming', 'settings-admin');
script('theming', '3rdparty/jscolor/jscolor');
style('theming', 'settings-admin');
?>
<div id="theming" class="section">
<h2 class="inlineblock"><?php p($l->t('Theming')); ?></h2>
<a target="_blank" rel="noreferrer" class="icon-info" title="<?php p($l->t('Open documentation'));?>" href="<?php p(link_to_docs('admin-theming')); ?>"></a>
<p class="settings-hint"><?php p($l->t('Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')); ?></p>
<div id="theming_settings_status">
<div id="theming_settings_loading" class="icon-loading-small" style="display: none;"></div>
<span id="theming_settings_msg" class="msg success" style="display: none;">Saved</span>
</div>
<?php if ($_['themable'] === false) { ?>
<p>
<?php p($_['errorMessage']) ?>
</p>
<?php } ?>
<div>
<label>
<span><?php p($l->t('Name')) ?></span>
<input id="theming-name" type="text" placeholder="<?php p($l->t('Name')); ?>" value="<?php p($_['name']) ?>" maxlength="250" />
<div data-setting="name" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</label>
</div>
<div>
<label>
<span><?php p($l->t('Web link')) ?></span>
<input id="theming-url" type="url" placeholder="<?php p($l->t('https://…')); ?>" value="<?php p($_['url']) ?>" maxlength="500" />
<div data-setting="url" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</label>
</div>
<div>
<label>
<span><?php p($l->t('Slogan')) ?></span>
<input id="theming-slogan" type="text" placeholder="<?php p($l->t('Slogan')); ?>" value="<?php p($_['slogan']) ?>" maxlength="500" />
<div data-setting="slogan" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</label>
</div>
<div>
<label>
<span><?php p($l->t('Color')) ?></span>
<input id="theming-color" type="text" maxlength="7" value="<?php p($_['color']) ?>" />
<div data-setting="color" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</label>
</div>
<div>
<form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="logo">
<input type="hidden" id="theming-logoMime" value="<?php p($_['images']['logo']['mime']); ?>" />
<input type="hidden" name="key" value="logo" />
<label for="uploadlogo"><span><?php p($l->t('Logo')) ?></span></label>
<input id="uploadlogo" class="fileupload" name="image" type="file" />
<label for="uploadlogo" class="button icon-upload svg" id="uploadlogo" title="<?php p($l->t('Upload new logo')) ?>"></label>
<div data-setting="logoMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</form>
</div>
<div>
<form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="background">
<input type="hidden" id="theming-backgroundMime" value="<?php p($_['images']['background']['mime']); ?>" />
<input type="hidden" name="key" value="background" />
<label for="upload-login-background"><span><?php p($l->t('Background and login image')) ?></span></label>
<input id="upload-login-background" class="fileupload" name="image" type="file">
<label for="upload-login-background" class="button icon-upload svg" id="upload-login-background" title="<?php p($l->t("Upload new login background")) ?>"></label>
<div data-setting="backgroundMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
<div class="theme-remove-bg icon icon-delete" data-toggle="tooltip" data-original-title="<?php p($l->t('Remove background image')); ?>"></div>
</form>
</div>
<div id="theming-preview">
<div id="theming-preview-logo"></div>
</div>
<h3 class="inlineblock"><?php p($l->t('Advanced options')); ?></h3>
<div class="advanced-options">
<div>
<label>
<span><?php p($l->t('Legal notice link')) ?></span>
<input id="theming-imprintUrl" type="url" placeholder="<?php p($l->t('https://…')); ?>" value="<?php p($_['imprintUrl']) ?>" maxlength="500" />
<div data-setting="imprintUrl" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</label>
</div>
<div>
<label>
<span><?php p($l->t('Privacy policy link')) ?></span>
<input id="theming-privacyUrl" type="url" placeholder="<?php p($l->t('https://…')); ?>" value="<?php p($_['privacyUrl']) ?>" maxlength="500" />
<div data-setting="privacyUrl" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</label>
</div>
<div class="advanced-option-logoheader">
<form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="logoheader">
<input type="hidden" id="theming-logoheaderMime" value="<?php p($_['images']['logoheader']['mime']); ?>" />
<input type="hidden" name="key" value="logoheader" />
<label for="upload-login-logoheader"><span><?php p($l->t('Header logo')) ?></span></label>
<input id="upload-login-logoheader" class="fileupload" name="image" type="file">
<label for="upload-login-logoheader" class="button icon-upload svg" id="upload-login-logoheader" title="<?php p($l->t("Upload new header logo")) ?>"></label>
<div id="theming-preview-logoheader" class="image-preview"></div>
<div data-setting="logoheaderMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</form>
</div>
<div class="advanced-option-favicon">
<form class="uploadButton" method="post" action="<?php p($_['uploadLogoRoute']) ?>" data-image-key="favicon">
<input type="hidden" id="theming-faviconMime" value="<?php p($_['images']['favicon']['mime']); ?>" />
<input type="hidden" name="key" value="favicon" />
<label for="upload-login-favicon"><span><?php p($l->t('Favicon')) ?></span></label>
<input id="upload-login-favicon" class="fileupload" name="image" type="file">
<label for="upload-login-favicon" class="button icon-upload svg" id="upload-login-favicon" title="<?php p($l->t("Upload new favicon")) ?>"></label>
<div id="theming-preview-favicon" class="image-preview"></div>
<div data-setting="faviconMime" data-toggle="tooltip" data-original-title="<?php p($l->t('Reset to default')); ?>" class="theme-undo icon icon-history"></div>
</form>
</div>
<div class="advanced-options" id="user-theming">
<label><span><?php p($l->t('User settings')); ?></span></label>
<div>
<p class="info">
<?php p($l->t('Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can check this box.')); ?>
</p>
<input id="userThemingDisabled" class="checkbox" type="checkbox" <?php p($_['userThemingDisabled'] ? 'checked="checked"' : ''); ?> />
<label for="userThemingDisabled"><?php p($l->t('Disable user theming')) ?></label>
</div>
</div>
</div>
<div class="theming-hints">
<?php if (!$_['canThemeIcons']) { ?>
<p class="info">
<a href="<?php p($_['iconDocs']); ?> target="_blank" rel="noreferrer noopener">
<em>
<?php p($l->t('Install the Imagemagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.')); ?>
</em>
</a>
</p>
<?php } ?>
</div>
</div>
<div id="admin-theming"></div>

View File

@ -32,30 +32,27 @@ use OCA\Theming\ImageManager;
use OCA\Theming\Settings\Admin;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IURLGenerator;
use Test\TestCase;
class AdminTest extends TestCase {
/** @var Admin */
private $admin;
/** @var IConfig */
private $config;
/** @var ThemingDefaults */
private $themingDefaults;
/** @var IURLGenerator */
private $urlGenerator;
/** @var ImageManager */
private $imageManager;
/** @var IL10N */
private $l10n;
private Admin $admin;
private IConfig $config;
private ThemingDefaults $themingDefaults;
private IInitialState $initialState;
private IURLGenerator $urlGenerator;
private ImageManager $imageManager;
private IL10N $l10n;
protected function setUp(): void {
parent::setUp();
$this->config = $this->createMock(IConfig::class);
$this->l10n = $this->createMock(IL10N::class);
$this->themingDefaults = $this->createMock(ThemingDefaults::class);
$this->initialState = $this->createMock(IInitialState::class);
$this->urlGenerator = $this->createMock(IURLGenerator::class);
$this->imageManager = $this->createMock(ImageManager::class);
@ -64,6 +61,7 @@ class AdminTest extends TestCase {
$this->config,
$this->l10n,
$this->themingDefaults,
$this->initialState,
$this->urlGenerator,
$this->imageManager
);
@ -99,28 +97,8 @@ class AdminTest extends TestCase {
->expects($this->once())
->method('getDefaultColorPrimary')
->willReturn('#fff');
$this->urlGenerator
->expects($this->once())
->method('linkToRoute')
->with('theming.Theming.uploadImage')
->willReturn('/my/route');
$params = [
'themable' => true,
'errorMessage' => '',
'name' => 'MyEntity',
'url' => 'https://example.com',
'slogan' => 'MySlogan',
'color' => '#fff',
'uploadLogoRoute' => '/my/route',
'canThemeIcons' => null,
'iconDocs' => null,
'images' => [],
'imprintUrl' => '',
'privacyUrl' => '',
'userThemingDisabled' => false,
];
$expected = new TemplateResponse('theming', 'settings-admin', $params, '');
$expected = new TemplateResponse('theming', 'settings-admin');
$this->assertEquals($expected, $this->admin->getForm());
}
@ -159,28 +137,8 @@ class AdminTest extends TestCase {
->expects($this->once())
->method('getDefaultColorPrimary')
->willReturn('#fff');
$this->urlGenerator
->expects($this->once())
->method('linkToRoute')
->with('theming.Theming.uploadImage')
->willReturn('/my/route');
$params = [
'themable' => false,
'errorMessage' => 'You are already using a custom theme. Theming app settings might be overwritten by that.',
'name' => 'MyEntity',
'url' => 'https://example.com',
'slogan' => 'MySlogan',
'color' => '#fff',
'uploadLogoRoute' => '/my/route',
'canThemeIcons' => null,
'iconDocs' => '',
'images' => [],
'imprintUrl' => '',
'privacyUrl' => '',
'userThemingDisabled' => false
];
$expected = new TemplateResponse('theming', 'settings-admin', $params, '');
$expected = new TemplateResponse('theming', 'settings-admin');
$this->assertEquals($expected, $this->admin->getForm());
}

Binary file not shown.

BIN
dist/core-common.js vendored

Binary file not shown.

Binary file not shown.

BIN
dist/theming-admin-theming.js vendored Normal file

Binary file not shown.

BIN
dist/theming-admin-theming.js.map vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
dist/theming-personal-theming.js.map vendored Normal file

Binary file not shown.

Binary file not shown.

View File

@ -1,31 +1,34 @@
@apache
Feature: app-theming
# FIXME test with cypress
# The existing DOM testing framework used here is not fully suitable for testing UIs implemented with modern frontend frameworks like Vue
Scenario: changing the color updates the primary color
Given I am logged in as the admin
And I visit the admin settings page
And I open the "Theming" section
And I see that the color selector in the Theming app has loaded
# And I see that the color selector in the Theming app has loaded
# The "eventually" part is not really needed here, as the colour is not
# being animated at this point, but there is no need to create a specific
# step just for this.
And I see that the primary color is eventually "#00639a"
And I see that the non-plain background color variable is eventually "#0082c9"
When I set the "Color" parameter in the Theming app to "#C9C9C9"
Then I see that the parameters in the Theming app are eventually saved
And I see that the primary color is eventually "#00639a"
And I see that the non-plain background color variable is eventually "#C9C9C9"
# And I see that the primary color is eventually "#00639a"
# And I see that the non-plain background color variable is eventually "#0082c9"
# When I set the "Color" parameter in the Theming app to "#C9C9C9"
# Then I see that the parameters in the Theming app are eventually saved
# And I see that the primary color is eventually "#00639a"
# And I see that the non-plain background color variable is eventually "#C9C9C9"
Scenario: resetting the color updates the primary color
Given I am logged in as the admin
And I visit the admin settings page
And I open the "Theming" section
And I see that the color selector in the Theming app has loaded
And I set the "Color" parameter in the Theming app to "#C9C9C9"
And I see that the parameters in the Theming app are eventually saved
And I see that the primary color is eventually "#00639a"
And I see that the non-plain background color variable is eventually "#C9C9C9"
When I reset the "Color" parameter in the Theming app to its default value
Then I see that the parameters in the Theming app are eventually saved
And I see that the primary color is eventually "#00639a"
And I see that the non-plain background color variable is eventually "#0082c9"
# And I see that the color selector in the Theming app has loaded
# And I set the "Color" parameter in the Theming app to "#C9C9C9"
# And I see that the parameters in the Theming app are eventually saved
# And I see that the primary color is eventually "#00639a"
# And I see that the non-plain background color variable is eventually "#C9C9C9"
# When I reset the "Color" parameter in the Theming app to its default value
# Then I see that the parameters in the Theming app are eventually saved
# And I see that the primary color is eventually "#00639a"
# And I see that the non-plain background color variable is eventually "#0082c9"

View File

@ -94,9 +94,14 @@ class ThemingAppContext implements Context, ActorAwareInterface {
$actor = $this->actor;
$colorSelectorLoadedCallback = function () use ($actor) {
$colorSelectorValue = $this->getRGBArray($actor->getSession()->evaluateScript("return $('#theming-color')[0].value;"));
$inputBgColor = $this->getRGBArray($actor->getSession()->evaluateScript("return $('#theming-color').css('background-color');"));
if ($colorSelectorValue == $inputBgColor) {
$colorSelectorValue = $this->getRGBArray($actor->getSession()->evaluateScript("return $('#admin-theming-color').text().trim();"));
$inputBgColorRgb = $this->getRGBArray($actor->getSession()->evaluateScript("return $('#admin-theming-color').css('background-color');"));
$matches = [];
preg_match_all('/\d+/', $inputBgColorRgb, $matches);
$inputBgColorHex = sprintf("#%02x%02x%02x", $matches[0][0], $matches[0][1], $matches[0][2]);
if ($colorSelectorValue == $inputBgColorHex) {
return true;
}

View File

@ -93,7 +93,8 @@ module.exports = {
systemtags: path.join(__dirname, 'apps/systemtags/src', 'systemtags.js'),
},
theming: {
'theming-settings': path.join(__dirname, 'apps/theming/src', 'settings.js'),
'personal-theming': path.join(__dirname, 'apps/theming/src', 'personal-settings.js'),
'admin-theming': path.join(__dirname, 'apps/theming/src', 'admin-settings.js'),
},
twofactor_backupcodes: {
settings: path.join(__dirname, 'apps/twofactor_backupcodes/src', 'settings.js'),