mirror of
https://github.com/nextcloud/server.git
synced 2026-04-21 22:27:31 -04:00
feat(theming): Allow users to configure their primary color
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
482395ba2f
commit
4d865fd33f
4 changed files with 156 additions and 11 deletions
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
130
apps/theming/src/components/UserPrimaryColor.vue
Normal file
130
apps/theming/src/components/UserPrimaryColor.vue
Normal file
|
|
@ -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…
Reference in a new issue