aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-02-16 15:13:27 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2024-05-21 20:36:21 +0200
commit752e3b9000e826207f1361373c6964c708868930 (patch)
tree11a2688cbf407d5e800056b4c167e067cd4b8980 /apps/theming
parentc8260924d60c80c4d1fec25d93979a84509701f7 (diff)
downloadnextcloud-server-752e3b9000e826207f1361373c6964c708868930.tar.gz
nextcloud-server-752e3b9000e826207f1361373c6964c708868930.zip
fix(settings): Make background selector be responsive to user changes
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
Diffstat (limited to 'apps/theming')
-rw-r--r--apps/theming/lib/Listener/BeforeTemplateRenderedListener.php38
-rw-r--r--apps/theming/lib/Settings/Admin.php4
-rw-r--r--apps/theming/lib/Settings/Personal.php21
-rw-r--r--apps/theming/src/AdminTheming.vue174
-rw-r--r--apps/theming/src/UserTheming.vue (renamed from apps/theming/src/UserThemes.vue)41
-rw-r--r--apps/theming/src/admin-settings.js2
-rw-r--r--apps/theming/src/components/BackgroundSettings.vue56
-rw-r--r--apps/theming/src/components/UserPrimaryColor.vue54
-rw-r--r--apps/theming/src/components/admin/ColorPickerField.vue49
-rw-r--r--apps/theming/src/components/admin/FileInputField.vue5
-rw-r--r--apps/theming/src/helpers/refreshStyles.js20
-rw-r--r--apps/theming/src/mixins/admin/TextValueMixin.js8
-rw-r--r--apps/theming/src/personal-settings.js2
13 files changed, 275 insertions, 199 deletions
diff --git a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
index d845c9a1091..f3ffa66e8fe 100644
--- a/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
+++ b/apps/theming/lib/Listener/BeforeTemplateRenderedListener.php
@@ -26,7 +26,6 @@ declare(strict_types=1);
namespace OCA\Theming\Listener;
use OCA\Theming\AppInfo\Application;
-use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\JSDataService;
use OCA\Theming\Service\ThemeInjectionService;
use OCP\AppFramework\Http\Events\BeforeLoginTemplateRenderedEvent;
@@ -81,43 +80,6 @@ class BeforeTemplateRenderedListener implements IEventListener {
$this->themeInjectionService->injectHeaders();
- $user = $this->userSession->getUser();
-
- if (!empty($user)) {
- $userId = $user->getUID();
-
- /** User background */
- $this->initialState->provideInitialState(
- '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_color', BackgroundService::DEFAULT_COLOR),
- );
-
- /**
- * Admin background. `backgroundColor` if disabled,
- * mime type if defined and empty by default
- */
- $this->initialState->provideInitialState(
- 'themingDefaultBackground',
- $this->config->getAppValue('theming', 'backgroundMime', ''),
- );
- $this->initialState->provideInitialState(
- 'defaultShippedBackground',
- BackgroundService::DEFAULT_BACKGROUND_IMAGE,
- );
-
- /** List of all shipped backgrounds */
- $this->initialState->provideInitialState(
- 'shippedBackgrounds',
- BackgroundService::SHIPPED_BACKGROUNDS,
- );
- }
-
// Making sure to inject just after core
\OCP\Util::addScript('theming', 'theming', 'core');
}
diff --git a/apps/theming/lib/Settings/Admin.php b/apps/theming/lib/Settings/Admin.php
index 8a34432a073..0198af6e53a 100644
--- a/apps/theming/lib/Settings/Admin.php
+++ b/apps/theming/lib/Settings/Admin.php
@@ -30,6 +30,7 @@ namespace OCA\Theming\Settings;
use OCA\Theming\AppInfo\Application;
use OCA\Theming\Controller\ThemingController;
use OCA\Theming\ImageManager;
+use OCA\Theming\Service\BackgroundService;
use OCA\Theming\ThemingDefaults;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
@@ -79,6 +80,9 @@ class Admin implements IDelegatedSettings {
'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
'logoMime' => $this->config->getAppValue(Application::APP_ID, 'logoMime', ''),
'allowedMimeTypes' => $allowedMimeTypes,
+ 'backgroundURL' => $this->imageManager->getImageUrl('background'),
+ 'defaultBackgroundURL' => $this->urlGenerator->linkTo(Application::APP_ID, 'img/background/' . BackgroundService::DEFAULT_BACKGROUND_IMAGE),
+ 'defaultBackgroundColor' => BackgroundService::DEFAULT_BACKGROUND_COLOR,
'backgroundMime' => $this->config->getAppValue(Application::APP_ID, 'backgroundMime', ''),
'logoheaderMime' => $this->config->getAppValue(Application::APP_ID, 'logoheaderMime', ''),
'faviconMime' => $this->config->getAppValue(Application::APP_ID, 'faviconMime', ''),
diff --git a/apps/theming/lib/Settings/Personal.php b/apps/theming/lib/Settings/Personal.php
index f24aaa2f8f8..3f497c1b8d2 100644
--- a/apps/theming/lib/Settings/Personal.php
+++ b/apps/theming/lib/Settings/Personal.php
@@ -26,6 +26,7 @@
namespace OCA\Theming\Settings;
use OCA\Theming\ITheme;
+use OCA\Theming\Service\BackgroundService;
use OCA\Theming\Service\ThemesService;
use OCA\Theming\ThemingDefaults;
use OCP\App\IAppManager;
@@ -71,6 +72,26 @@ class Personal implements ISettings {
// Get the default app enforced by admin
$forcedDefaultApp = $this->appManager->getDefaultAppForUser(null, false);
+ /** List of all shipped backgrounds */
+ $this->initialStateService->provideInitialState('shippedBackgrounds', BackgroundService::SHIPPED_BACKGROUNDS);
+
+ /**
+ * Admin theming
+ */
+ $this->initialStateService->provideInitialState('themingDefaults', [
+ /** URL of admin configured background image */
+ 'backgroundImage' => $this->themingDefaults->getBackground(),
+ /** `backgroundColor` if disabled, mime type if defined and empty by default */
+ 'backgroundMime' => $this->config->getAppValue('theming', 'backgroundMime', ''),
+ /** Admin configured background color */
+ 'backgroundColor' => $this->themingDefaults->getDefaultColorBackground(),
+ /** Admin configured primary color */
+ 'primaryColor' => $this->themingDefaults->getDefaultColorPrimary(),
+ /** Nextcloud default background image */
+ 'defaultShippedBackground' => BackgroundService::DEFAULT_BACKGROUND_IMAGE,
+ ]);
+
+ $this->initialStateService->provideInitialState('userBackgroundImage', $this->config->getUserValue($this->userId, 'theming', 'background_image', BackgroundService::BACKGROUND_DEFAULT));
$this->initialStateService->provideInitialState('themes', array_values($themes));
$this->initialStateService->provideInitialState('enforceTheme', $enforcedTheme);
$this->initialStateService->provideInitialState('isUserThemingDisabled', $this->themingDefaults->isUserThemingDisabled());
diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue
index f0831d8e944..db2003730dc 100644
--- a/apps/theming/src/AdminTheming.vue
+++ b/apps/theming/src/AdminTheming.vue
@@ -44,7 +44,7 @@
:placeholder="field.placeholder"
:type="field.type"
:value.sync="field.value"
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<!-- Primary color picker -->
<ColorPickerField :name="primaryColorPickerField.name"
@@ -53,33 +53,41 @@
:display-name="primaryColorPickerField.displayName"
:value.sync="primaryColorPickerField.value"
data-admin-theming-setting-primary-color
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<!-- Background color picker -->
- <ColorPickerField :name="backgroundColorPickerField.name"
- :description="backgroundColorPickerField.description"
- :default-value="defaultBackground"
- :display-name="backgroundColorPickerField.displayName"
- :value.sync="backgroundColorPickerField.value"
- data-admin-theming-setting-primary-color
- @update:theming="$emit('update:theming')" />
+ <ColorPickerField name="background_color"
+ :description="t('theming', 'Instead of a background image you can also configure a plain background color. If you use a background image changing this color will influence the color of the app menu icons.')"
+ :default-value.sync="defaultBackgroundColor"
+ :display-name="t('theming', 'Background color')"
+ :value.sync="backgroundColor"
+ data-admin-theming-setting-background-color
+ @update:theming="refreshStyles" />
<!-- Default background picker -->
- <FileInputField v-for="field in fileInputFields"
- :key="field.name"
- :aria-label="field.ariaLabel"
- :data-admin-theming-setting-file="field.name"
- :default-mime-value="field.defaultMimeValue"
- :display-name="field.displayName"
- :mime-name="field.mimeName"
- :mime-value.sync="field.mimeValue"
- :name="field.name"
- @update:theming="$emit('update:theming')" />
+ <FileInputField :aria-label="t('theming', 'Upload new logo')"
+ data-admin-theming-setting-file="logo"
+ :display-name="t('theming', 'Logo')"
+ mime-name="logoMime"
+ :mime-value.sync="logoMime"
+ name="logo"
+ @update:theming="refreshStyles" />
+
+ <FileInputField :aria-label="t('theming', 'Upload new background and login image')"
+ data-admin-theming-setting-file="background"
+ :display-name="t('theming', 'Background and login image')"
+ mime-name="backgroundMime"
+ :mime-value.sync="backgroundMime"
+ name="background"
+ @uploaded="backgroundURL = $event"
+ @update:theming="refreshStyles" />
+
<div class="admin-theming__preview" data-admin-theming-preview>
<div class="admin-theming__preview-logo" data-admin-theming-preview-logo />
</div>
</div>
</NcSettingsSection>
+
<NcSettingsSection :name="t('theming', 'Advanced options')">
<div class="admin-theming-advanced">
<TextField v-for="field in advancedTextFields"
@@ -91,7 +99,7 @@
:display-name="field.displayName"
:placeholder="field.placeholder"
:maxlength="field.maxlength"
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<FileInputField v-for="field in advancedFileInputFields"
:key="field.name"
:name="field.name"
@@ -100,7 +108,7 @@
:default-mime-value="field.defaultMimeValue"
:display-name="field.displayName"
:aria-label="field.ariaLabel"
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<CheckboxField :name="userThemingField.name"
:value="userThemingField.value"
:default-value="userThemingField.defaultValue"
@@ -108,7 +116,7 @@
:label="userThemingField.label"
:description="userThemingField.description"
data-admin-theming-setting-disable-user-theming
- @update:theming="$emit('update:theming')" />
+ @update:theming="refreshStyles" />
<a v-if="!canThemeIcons"
:href="docUrlIcons"
rel="noreferrer noopener">
@@ -122,6 +130,7 @@
<script>
import { loadState } from '@nextcloud/initial-state'
+import { refreshStyles } from './helpers/refreshStyles.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
@@ -132,7 +141,10 @@ import TextField from './components/admin/TextField.vue'
import AppMenuSection from './components/admin/AppMenuSection.vue'
const {
+ defaultBackgroundURL,
+
backgroundMime,
+ backgroundURL,
backgroundColor,
canThemeIcons,
docUrl,
@@ -190,32 +202,6 @@ const primaryColorPickerField = {
description: t('theming', 'The primary color is used for highlighting elements like important buttons. It might get slightly adjusted depending on the current color schema.'),
}
-const backgroundColorPickerField = {
- name: 'background_color',
- value: backgroundColor,
- displayName: t('theming', 'Background color'),
- description: t('theming', 'Instead of a background image you can also configure a plain background color. If you use a background image changing this color will influence the color of the app menu icons.'),
-}
-
-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',
@@ -278,18 +264,17 @@ export default {
TextField,
},
- emits: [
- 'update:theming',
- ],
-
- textFields,
-
data() {
return {
+ backgroundMime,
+ backgroundURL,
+ backgroundColor,
+ defaultBackgroundColor: '#0069c3',
+
+ logoMime,
+
textFields,
- backgroundColorPickerField,
primaryColorPickerField,
- fileInputFields,
advancedTextFields,
advancedFileInputFields,
userThemingField,
@@ -300,34 +285,64 @@ export default {
docUrlIcons,
isThemable,
notThemableErrorMessage,
-
- defaultBackground: this.calculateDefaultBackground(),
}
},
+ computed: {
+ cssBackgroundImage() {
+ if (this.backgroundURL) {
+ return `url('${this.backgroundURL}')`
+ }
+ return 'unset'
+ },
+ },
+
watch: {
- backgroundColorPickerField: {
- deep: true,
- handler() {
- this.defaultBackground = this.calculateDefaultBackground()
- },
+ backgroundMime() {
+ if (this.backgroundMime === '') {
+ // Reset URL to default value for preview
+ this.backgroundURL = defaultBackgroundURL
+ } else if (this.backgroundMime === 'backgroundColor') {
+ // Reset URL to empty image when only color is configured
+ this.backgroundURL = ''
+ }
},
+ async backgroundURL() {
+ // When the background is changed we need to emulate the background color change
+ if (this.backgroundURL !== '') {
+ const color = await this.calculateDefaultBackground()
+ this.defaultBackgroundColor = color
+ this.backgroundColor = color
+ }
+ },
+ },
+
+ async mounted() {
+ if (this.backgroundURL) {
+ this.defaultBackgroundColor = await this.calculateDefaultBackground()
+ }
},
methods: {
+ refreshStyles,
+
+ /**
+ * Same as on server - if a user uploads an image the mean color will be set as the background color
+ */
calculateDefaultBackground() {
const toHex = (num) => `00${num.toString(16)}`.slice(-2)
- const style = window.getComputedStyle(document.body).backgroundImage
- const match = style.match(/url\("(http.+)"\)/)
- if (!match) {
- return '#0082c9'
- }
- const context = document.createElement('canvas').getContext('2d')
- const img = new Image()
- img.src = match[1]
- context.imageSmoothingEnabled = true
- context.drawImage(img, 0, 0, 1, 1)
- return '#' + [...context.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(toHex).join('')
+
+ return new Promise((resolve, reject) => {
+ const img = new Image()
+ img.src = this.backgroundURL
+ img.onload = () => {
+ const context = document.createElement('canvas').getContext('2d')
+ context.imageSmoothingEnabled = true
+ context.drawImage(img, 0, 0, 1, 1)
+ resolve('#' + [...context.getImageData(0, 0, 1, 1).data.slice(0, 3)].map(toHex).join(''))
+ }
+ img.onerror = reject
+ })
},
},
}
@@ -349,15 +364,8 @@ export default {
background-position: center;
text-align: center;
margin-top: 10px;
- /* This is basically https://github.com/nextcloud/server/blob/master/core/css/guest.css
- But without the user variables. That way the admin can preview the render as guest*/
- /* As guest, there is no user color color-background-plain */
- background-color: var(--color-primary-element-default);
- /* As guest, there is no user background (--image-background)
- 1. Empty background if defined
- 2. Else default background
- 3. Finally default gradient (should not happened, the background is always defined anyway) */
- background-image: var(--image-background-plain, var(--image-background-default));
+ background-color: v-bind('backgroundColor');
+ background-image: v-bind('cssBackgroundImage');
&-logo {
width: 20%;
diff --git a/apps/theming/src/UserThemes.vue b/apps/theming/src/UserTheming.vue
index 900577754b5..28fb20ad59c 100644
--- a/apps/theming/src/UserThemes.vue
+++ b/apps/theming/src/UserTheming.vue
@@ -57,19 +57,19 @@
:description="isUserThemingDisabled
? t('theming', 'Customization has been disabled by your administrator')
: t('theming', 'Set a primary color to highlight important elements. The color used for elements such as primary buttons might differ a bit as it gets adjusted to fulfill accessibility requirements.')">
- <UserPrimaryColor @refresh-styles="refreshGlobalStyles" />
+ <UserPrimaryColor v-if="!isUserThemingDisabled"
+ ref="primaryColor"
+ @refresh-styles="refreshGlobalStyles" />
</NcSettingsSection>
- <NcSettingsSection :name="t('theming', 'Background and color')"
- class="background"
- data-user-theming-background-disabled>
- <template v-if="isUserThemingDisabled">
- <p>{{ t('theming', 'Customization has been disabled by your administrator') }}</p>
- </template>
- <template v-else>
- <p>{{ t('theming', 'The background can be set to an image from the default set, a custom uploaded image, or a plain color. The primary color will automatically be adapted based on this and used for elements like folder icons, primary buttons and highlights.') }}</p>
- <BackgroundSettings class="background__grid" @update:background="refreshGlobalStyles" />
- </template>
+ <NcSettingsSection class="background"
+ :name="t('theming', 'Background and color')"
+ :description="isUserThemingDisabled
+ ? t('theming', 'Customization has been disabled by your administrator')
+ : t('theming', 'The background can be set to an image from the default set, a custom uploaded image, or a plain color.')">
+ <BackgroundSettings v-if="!isUserThemingDisabled"
+ class="background__grid"
+ @update:background="refreshGlobalStyles" />
</NcSettingsSection>
<NcSettingsSection :name="t('theming', 'Keyboard shortcuts')"
@@ -89,7 +89,10 @@
<script>
import { generateOcsUrl } from '@nextcloud/router'
import { loadState } from '@nextcloud/initial-state'
+import { refreshStyles } from './helpers/refreshStyles'
+
import axios from '@nextcloud/axios'
+
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
@@ -97,7 +100,6 @@ import BackgroundSettings from './components/BackgroundSettings.vue'
import ItemPreview from './components/ItemPreview.vue'
import UserAppMenuSection from './components/UserAppMenuSection.vue'
import UserPrimaryColor from './components/UserPrimaryColor.vue'
-import { emit } from '@nextcloud/event-bus'
const availableThemes = loadState('theming', 'themes', [])
const enforceTheme = loadState('theming', 'enforceTheme', '')
@@ -106,7 +108,7 @@ const shortcutsDisabled = loadState('theming', 'shortcutsDisabled', false)
const isUserThemingDisabled = loadState('theming', 'isUserThemingDisabled')
export default {
- name: 'UserThemes',
+ name: 'UserTheming',
components: {
ItemPreview,
@@ -183,16 +185,9 @@ export default {
methods: {
// Refresh server-side generated theming CSS
- refreshGlobalStyles() {
- [...document.head.querySelectorAll('link.theme')].forEach(theme => {
- const url = new URL(theme.href)
- url.searchParams.set('v', Date.now())
- const newTheme = theme.cloneNode()
- newTheme.href = url.toString()
- newTheme.onload = () => theme.remove()
- document.head.append(newTheme)
- })
- emit('theming:global-styles-refreshed')
+ async refreshGlobalStyles() {
+ await refreshStyles()
+ this.$nextTick(() => this.$refs.primaryColor.reload())
},
changeTheme({ enabled, id }) {
diff --git a/apps/theming/src/admin-settings.js b/apps/theming/src/admin-settings.js
index 774d58e4a5c..b5d94064770 100644
--- a/apps/theming/src/admin-settings.js
+++ b/apps/theming/src/admin-settings.js
@@ -22,7 +22,6 @@
import { getRequestToken } from '@nextcloud/auth'
import Vue from 'vue'
-import { refreshStyles } from './helpers/refreshStyles.js'
import App from './AdminTheming.vue'
// eslint-disable-next-line camelcase
@@ -34,4 +33,3 @@ Vue.prototype.t = t
const View = Vue.extend(App)
const theming = new View()
theming.$mount('#admin-theming')
-theming.$on('update:theming', refreshStyles)
diff --git a/apps/theming/src/components/BackgroundSettings.vue b/apps/theming/src/components/BackgroundSettings.vue
index 1ccf05b3ce9..fd8a5169524 100644
--- a/apps/theming/src/components/BackgroundSettings.vue
+++ b/apps/theming/src/components/BackgroundSettings.vue
@@ -41,17 +41,19 @@
</button>
<!-- Custom color picker -->
- <NcColorPicker v-model="Theming.backgroundColor" @input="debouncePickColor">
+ <NcColorPicker v-model="Theming.backgroundColor" @update:value="debouncePickColor">
<button :class="{
'icon-loading': loading === 'color',
'background background__color': true,
'background--active': backgroundImage === 'color'
}"
+ :aria-pressed="backgroundImage === 'color'"
:data-color="Theming.backgroundColor"
:data-color-bright="invertTextColor(Theming.backgroundColor)"
:style="{ backgroundColor: Theming.backgroundColor, '--border-color': Theming.backgroundColor}"
data-user-theming-background-color
- tabindex="0">
+ tabindex="0"
+ @click="backgroundImage !== 'color' && debouncePickColor(Theming.backgroundColor)">
{{ t('theming', 'Plain background') /* TRANSLATORS: Background using a single color */ }}
<ColorPalette v-if="backgroundImage !== 'color'" :size="20" />
<Check :size="44" />
@@ -65,8 +67,8 @@
'background background__default': true,
'background--active': backgroundImage === 'default'
}"
- :data-color-bright="invertTextColor(Theming.defaultColor)"
- :style="{ '--border-color': Theming.defaultColor }"
+ :data-color-bright="invertTextColor(Theming.defaultBackgroundColor)"
+ :style="{ '--border-color': Theming.defaultBackgroundColor }"
data-user-theming-background-default
tabindex="0"
@click="setDefault">
@@ -85,6 +87,7 @@
'icon-loading': loading === shippedBackground.name,
'background--active': backgroundImage === shippedBackground.name
}"
+ :data-color-bright="invertTextColor(shippedBackground.details.background_color)"
:data-user-theming-background-shipped="shippedBackground.name"
:style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }"
tabindex="0"
@@ -109,10 +112,14 @@ import Check from 'vue-material-design-icons/Check.vue'
import ImageEdit from 'vue-material-design-icons/ImageEdit.vue'
import ColorPalette from 'vue-material-design-icons/Palette.vue'
-const backgroundImage = loadState('theming', 'backgroundImage')
const shippedBackgroundList = loadState('theming', 'shippedBackgrounds')
-const themingDefaultBackground = loadState('theming', 'themingDefaultBackground')
-const defaultShippedBackground = loadState('theming', 'defaultShippedBackground')
+const backgroundImage = loadState('theming', 'userBackgroundImage')
+const {
+ backgroundImage: defaultBackgroundImage,
+ backgroundColor: defaultBackgroundColor,
+ backgroundMime: defaultBackgroundMime,
+ defaultShippedBackground,
+} = loadState('theming', 'themingDefaults')
const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url
@@ -139,7 +146,12 @@ export default {
computed: {
shippedBackgrounds() {
return Object.keys(shippedBackgroundList)
- .map(fileName => {
+ .filter((background) => {
+ // If the admin did not changed the global background
+ // let's hide the default background to not show it twice
+ return background !== defaultShippedBackground || !this.isGlobalBackgroundDefault
+ })
+ .map((fileName) => {
return {
name: fileName,
url: prefixWithBaseUrl(fileName),
@@ -147,22 +159,18 @@ export default {
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
+ return defaultBackgroundMime === ''
},
isGlobalBackgroundDeleted() {
- return themingDefaultBackground === 'backgroundColor'
+ return defaultBackgroundMime === 'backgroundColor'
+ },
+
+ cssDefaultBackgroundImage() {
+ return `url('${defaultBackgroundImage}')`
},
},
@@ -241,12 +249,12 @@ export default {
this.update(result.data)
},
- async pickColor(event) {
+ async pickColor(color) {
this.loading = 'color'
- const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9'
- const result = await axios.post(generateUrl('/apps/theming/background/color'), { color })
- this.update(result.data)
+ const { data } = await axios.post(generateUrl('/apps/theming/background/color'), { color: color || '#0082c9' })
+ this.update(data)
},
+
debouncePickColor: debounce(function(...args) {
this.pickColor(...args)
}, 200),
@@ -361,8 +369,8 @@ export default {
}
&__default {
- background-color: var(--color-primary-default);
- background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), var(--image-background-plain, var(--image-background-default));
+ background-color: var(--color-background-plain);
+ background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), v-bind(cssDefaultBackgroundImage);
}
&__filepicker, &__default, &__color {
diff --git a/apps/theming/src/components/UserPrimaryColor.vue b/apps/theming/src/components/UserPrimaryColor.vue
index 5211b5aaba0..a27fc2499ab 100644
--- a/apps/theming/src/components/UserPrimaryColor.vue
+++ b/apps/theming/src/components/UserPrimaryColor.vue
@@ -1,14 +1,18 @@
<template>
<div class="primary-color__wrapper">
- <NcColorPicker v-model="primaryColor" @submit="onUpdate">
+ <NcColorPicker v-model="primaryColor"
+ data-user-theming-primary-color
+ @update:value="debouncedOnUpdate">
<button ref="trigger"
- class="color-container primary-color__trigger">
+ class="color-container primary-color__trigger"
+ :style="{ 'background-color': primaryColor }"
+ data-user-theming-primary-color-trigger>
{{ t('theming', 'Primary color') }}
<NcLoadingIcon v-if="loading" />
<IconColorPalette v-else :size="20" />
</button>
</NcColorPicker>
- <NcButton type="tertiary" :disabled="primaryColor === defaultColor" @click="primaryColor = defaultColor">
+ <NcButton type="tertiary" :disabled="isdefaultPrimaryColor" @click="onReset">
<template #icon>
<IconUndo :size="20" />
</template>
@@ -22,6 +26,8 @@ import { showError } from '@nextcloud/dialogs'
import { loadState } from '@nextcloud/initial-state'
import { translate as t } from '@nextcloud/l10n'
import { generateOcsUrl } from '@nextcloud/router'
+import { colord } from 'colord'
+import { debounce } from 'debounce'
import { defineComponent } from 'vue'
import axios from '@nextcloud/axios'
@@ -30,10 +36,12 @@ import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import IconColorPalette from 'vue-material-design-icons/Palette.vue'
import IconUndo from 'vue-material-design-icons/UndoVariant.vue'
-import { subscribe } from '@nextcloud/event-bus'
+
+const { primaryColor, defaultPrimaryColor } = loadState('theming', 'data', { primaryColor: '#0082c9', defaultPrimaryColor: '#0082c9' })
export default defineComponent({
name: 'UserPrimaryColor',
+
components: {
IconColorPalette,
IconUndo,
@@ -41,17 +49,24 @@ export default defineComponent({
NcColorPicker,
NcLoadingIcon,
},
+
+ emits: ['refresh-styles'],
+
data() {
- const { primaryColor, defaultColor } = loadState('theming', 'data', { primaryColor: '#0082c9', defaultColor: '#0082c9' })
return {
- defaultColor,
primaryColor,
loading: false,
}
},
- mounted() {
- subscribe('theming:global-styles-refreshed', this.onRefresh)
+ computed: {
+ isdefaultPrimaryColor() {
+ return colord(this.primaryColor).isEqual(colord(defaultPrimaryColor))
+ },
+
+ debouncedOnUpdate() {
+ return debounce(this.onUpdate, 500)
+ },
},
methods: {
@@ -60,23 +75,33 @@ export default defineComponent({
/**
* Global styles are reloaded so we might need to update the current value
*/
- onRefresh() {
- const newColor = window.getComputedStyle(this.$refs.trigger).backgroundColor
+ reload() {
+ const trigger = this.$refs.trigger as HTMLButtonElement
+ const newColor = window.getComputedStyle(trigger).backgroundColor
if (newColor.toLowerCase() !== this.primaryColor) {
this.primaryColor = newColor
}
},
- async onUpdate(value: string) {
+ onReset() {
+ this.primaryColor = defaultPrimaryColor
+ this.onUpdate(null)
+ },
+
+ async onUpdate(value: string | null) {
this.loading = true
const url = generateOcsUrl('apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', {
appId: 'theming',
configKey: 'primary_color',
})
try {
- await axios.post(url, {
- configValue: value,
- })
+ if (value) {
+ await axios.post(url, {
+ configValue: value,
+ })
+ } else {
+ await axios.delete(url)
+ }
this.$emit('refresh-styles')
} catch (e) {
console.error('Could not update primary color', e)
@@ -107,6 +132,7 @@ export default defineComponent({
background-color: var(--color-primary);
color: var(--color-primary-text);
width: 350px;
+ max-width: 100vw;
height: 96px;
word-wrap: break-word;
diff --git a/apps/theming/src/components/admin/ColorPickerField.vue b/apps/theming/src/components/admin/ColorPickerField.vue
index 1cac5418d7a..79958dd542b 100644
--- a/apps/theming/src/components/admin/ColorPickerField.vue
+++ b/apps/theming/src/components/admin/ColorPickerField.vue
@@ -31,18 +31,21 @@
class="field__button"
type="primary"
:aria-label="t('theming', 'Select a custom color')"
- data-admin-theming-setting-primary-color-picker>
+ data-admin-theming-setting-color-picker>
<template #icon>
- <Palette :size="20" />
+ <NcLoadingIcon v-if="loading"
+ :appearance="calculatedTextColor === '#ffffff' ? 'light' : 'dark'"
+ :size="20" />
+ <Palette v-else :size="20" />
</template>
{{ value }}
</NcButton>
</NcColorPicker>
- <div class="field__color-preview" data-admin-theming-setting-primary-color />
+ <div class="field__color-preview" data-admin-theming-setting-color />
<NcButton v-if="value !== defaultValue"
type="tertiary"
:aria-label="t('theming', 'Reset to default')"
- data-admin-theming-setting-primary-color-reset
+ data-admin-theming-setting-color-reset
@click="undo">
<template #icon>
<Undo :size="20" />
@@ -63,8 +66,10 @@
<script>
import { debounce } from 'debounce'
+import { colord } from 'colord'
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker.js'
+import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'
import Undo from 'vue-material-design-icons/UndoVariant.vue'
import Palette from 'vue-material-design-icons/Palette.vue'
@@ -77,6 +82,7 @@ export default {
components: {
NcButton,
NcColorPicker,
+ NcLoadingIcon,
NcNoteCard,
Undo,
Palette,
@@ -99,6 +105,10 @@ export default {
type: String,
required: true,
},
+ textColor: {
+ type: String,
+ default: null,
+ },
defaultValue: {
type: String,
required: true,
@@ -109,9 +119,33 @@ export default {
},
},
+ emits: ['update:theming'],
+
+ data() {
+ return {
+ loading: false,
+ }
+ },
+
+ computed: {
+ calculatedTextColor() {
+ const color = colord(this.value)
+ return color.isLight() ? '#000000' : '#ffffff'
+ },
+ usedTextColor() {
+ if (this.textColor) {
+ return this.textColor
+ }
+ return this.calculatedTextColor
+ },
+ },
+
methods: {
debounceSave: debounce(async function() {
+ this.loading = true
await this.save()
+ this.$emit('update:theming')
+ this.loading = false
}, 200),
},
}
@@ -124,10 +158,15 @@ export default {
}
.field {
+ &__button {
+ background-color: v-bind('value') !important;
+ color: v-bind('usedTextColor') !important;
+ }
+
&__color-preview {
width: var(--default-clickable-area);
border-radius: var(--border-radius-large);
- background-color: var(--color-primary-default);
+ background-color: v-bind('value');
}
}
</style>
diff --git a/apps/theming/src/components/admin/FileInputField.vue b/apps/theming/src/components/admin/FileInputField.vue
index 3d6fda9ec70..466068ab409 100644
--- a/apps/theming/src/components/admin/FileInputField.vue
+++ b/apps/theming/src/components/admin/FileInputField.vue
@@ -126,7 +126,7 @@ export default {
},
defaultMimeValue: {
type: String,
- required: true,
+ default: '',
},
displayName: {
type: String,
@@ -182,9 +182,10 @@ export default {
const url = generateUrl('/apps/theming/ajax/uploadImage')
try {
this.showLoading = true
- await axios.post(url, formData)
+ const { data } = await axios.post(url, formData)
this.showLoading = false
this.$emit('update:mime-value', file.type)
+ this.$emit('uploaded', data.data.url)
this.handleSuccess()
} catch (e) {
this.showLoading = false
diff --git a/apps/theming/src/helpers/refreshStyles.js b/apps/theming/src/helpers/refreshStyles.js
index 0c4a7cea22b..834be18c713 100644
--- a/apps/theming/src/helpers/refreshStyles.js
+++ b/apps/theming/src/helpers/refreshStyles.js
@@ -20,14 +20,24 @@
*
*/
-export const refreshStyles = () => {
- // Refresh server-side generated theming CSS
- [...document.head.querySelectorAll('link.theme')].forEach(theme => {
+/**
+ * Refresh server-side generated theming CSS
+ * This resolves when all themes are reloaded
+ */
+export async function refreshStyles() {
+ const themes = [...document.head.querySelectorAll('link.theme')]
+ const promises = themes.map((theme) => new Promise((resolve) => {
const url = new URL(theme.href)
url.searchParams.set('v', Date.now())
const newTheme = theme.cloneNode()
newTheme.href = url.toString()
- newTheme.onload = () => theme.remove()
+ newTheme.onload = () => {
+ theme.remove()
+ resolve()
+ }
document.head.append(newTheme)
- })
+ }))
+
+ // Wait until all themes are loaded
+ await Promise.allSettled(promises)
}
diff --git a/apps/theming/src/mixins/admin/TextValueMixin.js b/apps/theming/src/mixins/admin/TextValueMixin.js
index 4cce8bb301a..3565c63103d 100644
--- a/apps/theming/src/mixins/admin/TextValueMixin.js
+++ b/apps/theming/src/mixins/admin/TextValueMixin.js
@@ -64,10 +64,14 @@ export default {
this.reset()
const url = generateUrl('/apps/theming/ajax/undoChanges')
try {
- await axios.post(url, {
+ const { data } = await axios.post(url, {
setting: this.name,
})
- this.$emit('update:value', this.defaultValue)
+
+ if (data.data.value) {
+ this.$emit('update:defaultValue', data.data.value)
+ }
+ this.$emit('update:value', data.data.value || this.defaultValue)
this.handleSuccess()
} catch (e) {
this.errorMessage = e.response.data.data?.message
diff --git a/apps/theming/src/personal-settings.js b/apps/theming/src/personal-settings.js
index ba7bf2e0ee2..adbafaaf1ee 100644
--- a/apps/theming/src/personal-settings.js
+++ b/apps/theming/src/personal-settings.js
@@ -23,7 +23,7 @@ import { getRequestToken } from '@nextcloud/auth'
import Vue from 'vue'
import { refreshStyles } from './helpers/refreshStyles.js'
-import App from './UserThemes.vue'
+import App from './UserTheming.vue'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(getRequestToken())