aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorFerdinand Thiessen <opensource@fthiessen.de>2024-01-20 03:22:30 +0100
committerFerdinand Thiessen <opensource@fthiessen.de>2024-05-21 20:36:21 +0200
commit4d865fd33fcd309a241f9fd21144fe157ad4a3d2 (patch)
tree15ac9ef211c35ca61eeb9d4792a0833011df299e
parent482395ba2f12b71af115d07f7c99a10613118ac7 (diff)
downloadnextcloud-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.php16
-rw-r--r--apps/theming/src/UserThemes.vue20
-rw-r--r--apps/theming/src/components/BackgroundSettings.vue1
-rw-r--r--apps/theming/src/components/UserPrimaryColor.vue130
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>