diff options
author | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-01-20 03:22:30 +0100 |
---|---|---|
committer | Ferdinand Thiessen <opensource@fthiessen.de> | 2024-05-21 20:36:21 +0200 |
commit | 4d865fd33fcd309a241f9fd21144fe157ad4a3d2 (patch) | |
tree | 15ac9ef211c35ca61eeb9d4792a0833011df299e | |
parent | 482395ba2f12b71af115d07f7c99a10613118ac7 (diff) | |
download | nextcloud-server-4d865fd33fcd309a241f9fd21144fe157ad4a3d2.tar.gz nextcloud-server-4d865fd33fcd309a241f9fd21144fe157ad4a3d2.zip |
feat(theming): Allow users to configure their primary color
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
-rw-r--r-- | apps/theming/lib/Listener/BeforePreferenceListener.php | 16 | ||||
-rw-r--r-- | apps/theming/src/UserThemes.vue | 20 | ||||
-rw-r--r-- | apps/theming/src/components/BackgroundSettings.vue | 1 | ||||
-rw-r--r-- | apps/theming/src/components/UserPrimaryColor.vue | 130 |
4 files changed, 156 insertions, 11 deletions
diff --git a/apps/theming/lib/Listener/BeforePreferenceListener.php b/apps/theming/lib/Listener/BeforePreferenceListener.php index 47b7d3fb6ff..4981af6950e 100644 --- a/apps/theming/lib/Listener/BeforePreferenceListener.php +++ b/apps/theming/lib/Listener/BeforePreferenceListener.php @@ -55,14 +55,24 @@ class BeforePreferenceListener implements IEventListener { } private function handleThemingValues(BeforePreferenceSetEvent|BeforePreferenceDeletedEvent $event): void { - if ($event->getConfigKey() !== 'shortcuts_disabled') { + $allowedKeys = ['shortcuts_disabled', 'primary_color']; + + if (!in_array($event->getConfigKey(), $allowedKeys)) { // Not allowed config key return; } if ($event instanceof BeforePreferenceSetEvent) { - $event->setValid($event->getConfigValue() === 'yes'); - return; + switch ($event->getConfigKey()) { + case 'shortcuts_disabled': + $event->setValid($event->getConfigValue() === 'yes'); + break; + case 'primary_color': + $event->setValid(preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $event->getConfigValue()) === 1); + break; + default: + $event->setValid(false); + } } $event->setValid(true); diff --git a/apps/theming/src/UserThemes.vue b/apps/theming/src/UserThemes.vue index 676b8eca767..900577754b5 100644 --- a/apps/theming/src/UserThemes.vue +++ b/apps/theming/src/UserThemes.vue @@ -53,6 +53,13 @@ </div> </NcSettingsSection> + <NcSettingsSection :name="t('theming', 'Primary color')" + :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" /> + </NcSettingsSection> + <NcSettingsSection :name="t('theming', 'Background and color')" class="background" data-user-theming-background-disabled> @@ -65,8 +72,8 @@ </template> </NcSettingsSection> - <NcSettingsSection :name="t('theming', 'Keyboard shortcuts')"> - <p>{{ t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.') }}</p> + <NcSettingsSection :name="t('theming', 'Keyboard shortcuts')" + :description="t('theming', 'In some cases keyboard shortcuts can interfere with accessibility tools. In order to allow focusing on your tool correctly you can disable all keyboard shortcuts here. This will also disable all available shortcuts in apps.')"> <NcCheckboxRadioSwitch class="theming__preview-toggle" :checked.sync="shortcutsDisabled" type="switch" @@ -89,6 +96,8 @@ import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection. 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', '') @@ -105,6 +114,7 @@ export default { NcSettingsSection, BackgroundSettings, UserAppMenuSection, + UserPrimaryColor, }, data() { @@ -182,11 +192,7 @@ export default { newTheme.onload = () => theme.remove() document.head.append(newTheme) }) - }, - - updateBackground(data) { - this.background = (data.type === 'custom' || data.type === 'default') ? data.type : data.value - this.refreshGlobalStyles() + emit('theming:global-styles-refreshed') }, changeTheme({ enabled, id }) { diff --git a/apps/theming/src/components/BackgroundSettings.vue b/apps/theming/src/components/BackgroundSettings.vue index 166fdbc5f4b..1ccf05b3ce9 100644 --- a/apps/theming/src/components/BackgroundSettings.vue +++ b/apps/theming/src/components/BackgroundSettings.vue @@ -116,7 +116,6 @@ const defaultShippedBackground = loadState('theming', 'defaultShippedBackground' const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url -console.warn(loadState('theming', 'data', {})) export default { name: 'BackgroundSettings', diff --git a/apps/theming/src/components/UserPrimaryColor.vue b/apps/theming/src/components/UserPrimaryColor.vue new file mode 100644 index 00000000000..5211b5aaba0 --- /dev/null +++ b/apps/theming/src/components/UserPrimaryColor.vue @@ -0,0 +1,130 @@ +<template> + <div class="primary-color__wrapper"> + <NcColorPicker v-model="primaryColor" @submit="onUpdate"> + <button ref="trigger" + class="color-container 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"> + <template #icon> + <IconUndo :size="20" /> + </template> + {{ t('theming', 'Reset primary color') }} + </NcButton> + </div> +</template> + +<script lang="ts"> +import { showError } from '@nextcloud/dialogs' +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import { generateOcsUrl } from '@nextcloud/router' +import { defineComponent } from 'vue' + +import axios from '@nextcloud/axios' +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 IconColorPalette from 'vue-material-design-icons/Palette.vue' +import IconUndo from 'vue-material-design-icons/UndoVariant.vue' +import { subscribe } from '@nextcloud/event-bus' + +export default defineComponent({ + name: 'UserPrimaryColor', + components: { + IconColorPalette, + IconUndo, + NcButton, + NcColorPicker, + NcLoadingIcon, + }, + data() { + const { primaryColor, defaultColor } = loadState('theming', 'data', { primaryColor: '#0082c9', defaultColor: '#0082c9' }) + return { + defaultColor, + primaryColor, + loading: false, + } + }, + + mounted() { + subscribe('theming:global-styles-refreshed', this.onRefresh) + }, + + methods: { + t, + + /** + * Global styles are reloaded so we might need to update the current value + */ + onRefresh() { + const newColor = window.getComputedStyle(this.$refs.trigger).backgroundColor + if (newColor.toLowerCase() !== this.primaryColor) { + this.primaryColor = newColor + } + }, + + async onUpdate(value: string) { + 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, + }) + this.$emit('refresh-styles') + } catch (e) { + console.error('Could not update primary color', e) + showError(t('theming', 'Could not set primary color')) + } + this.loading = false + }, + }, +}) +</script> + +<style scoped lang="scss"> +.primary-color { + &__wrapper { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 12px; + } + + &__trigger { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + + background-color: var(--color-primary); + color: var(--color-primary-text); + width: 350px; + height: 96px; + + word-wrap: break-word; + hyphens: auto; + + border: 2px solid var(--color-main-background); + border-radius: var(--border-radius-large); + + &:active { + background-color: var(--color-primary-hover) !important; + } + + &:hover, + &:focus, + &:focus-visible { + border-color: var(--color-main-background) !important; + outline: 2px solid var(--color-main-text) !important; + } + } +} +</style> |