feat(theming): Allow users to configure their primary color

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/42977/head
Ferdinand Thiessen 10 months ago
parent 482395ba2f
commit 4d865fd33f
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
  1. 16
      apps/theming/lib/Listener/BeforePreferenceListener.php
  2. 20
      apps/theming/src/UserThemes.vue
  3. 1
      apps/theming/src/components/BackgroundSettings.vue
  4. 130
      apps/theming/src/components/UserPrimaryColor.vue

@ -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);

@ -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 }) {

@ -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',

@ -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>
Loading…
Cancel
Save