diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-02-16 15:13:27 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-05-21 20:36:21 +0200 |
commit | 752e3b9000e826207f1361373c6964c708868930 (patch) | |
tree | 11a2688cbf407d5e800056b4c167e067cd4b8980 /apps/theming | |
parent | c8260924d60c80c4d1fec25d93979a84509701f7 (diff) | |
download | nextcloud-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.php | 38 | ||||
-rw-r--r-- | apps/theming/lib/Settings/Admin.php | 4 | ||||
-rw-r--r-- | apps/theming/lib/Settings/Personal.php | 21 | ||||
-rw-r--r-- | apps/theming/src/AdminTheming.vue | 174 | ||||
-rw-r--r-- | apps/theming/src/UserTheming.vue (renamed from apps/theming/src/UserThemes.vue) | 41 | ||||
-rw-r--r-- | apps/theming/src/admin-settings.js | 2 | ||||
-rw-r--r-- | apps/theming/src/components/BackgroundSettings.vue | 56 | ||||
-rw-r--r-- | apps/theming/src/components/UserPrimaryColor.vue | 54 | ||||
-rw-r--r-- | apps/theming/src/components/admin/ColorPickerField.vue | 49 | ||||
-rw-r--r-- | apps/theming/src/components/admin/FileInputField.vue | 5 | ||||
-rw-r--r-- | apps/theming/src/helpers/refreshStyles.js | 20 | ||||
-rw-r--r-- | apps/theming/src/mixins/admin/TextValueMixin.js | 8 | ||||
-rw-r--r-- | apps/theming/src/personal-settings.js | 2 |
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()) |