diff options
Diffstat (limited to 'apps/theming/src/AdminTheming.vue')
-rw-r--r-- | apps/theming/src/AdminTheming.vue | 365 |
1 files changed, 365 insertions, 0 deletions
diff --git a/apps/theming/src/AdminTheming.vue b/apps/theming/src/AdminTheming.vue new file mode 100644 index 00000000000..e899024ca53 --- /dev/null +++ b/apps/theming/src/AdminTheming.vue @@ -0,0 +1,365 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <section> + <NcSettingsSection :name="t('theming', 'Theming')" + :description="t('theming', 'Theming makes it possible to easily customize the look and feel of your instance and supported clients. This will be visible for all users.')" + :doc-url="docUrl" + data-admin-theming-settings> + <div class="admin-theming"> + <NcNoteCard v-if="!isThemable" + type="error" + :show-alert="true"> + <p>{{ notThemableErrorMessage }}</p> + </NcNoteCard> + + <!-- Name, web link, slogan... fields --> + <TextField v-for="field in textFields" + :key="field.name" + :data-admin-theming-setting-field="field.name" + :default-value="field.defaultValue" + :display-name="field.displayName" + :maxlength="field.maxlength" + :name="field.name" + :placeholder="field.placeholder" + :type="field.type" + :value.sync="field.value" + @update:theming="refreshStyles" /> + + <!-- Primary color picker --> + <ColorPickerField :name="primaryColorPickerField.name" + :description="primaryColorPickerField.description" + :default-value="primaryColorPickerField.defaultValue" + :display-name="primaryColorPickerField.displayName" + :value.sync="primaryColorPickerField.value" + data-admin-theming-setting-primary-color + @update:theming="refreshStyles" /> + + <!-- Background color picker --> + <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 :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" + :key="field.name" + :name="field.name" + :value.sync="field.value" + :default-value="field.defaultValue" + :type="field.type" + :display-name="field.displayName" + :placeholder="field.placeholder" + :maxlength="field.maxlength" + @update:theming="refreshStyles" /> + <FileInputField v-for="field in advancedFileInputFields" + :key="field.name" + :name="field.name" + :mime-name="field.mimeName" + :mime-value.sync="field.mimeValue" + :default-mime-value="field.defaultMimeValue" + :display-name="field.displayName" + :aria-label="field.ariaLabel" + @update:theming="refreshStyles" /> + <CheckboxField :name="userThemingField.name" + :value="userThemingField.value" + :default-value="userThemingField.defaultValue" + :display-name="userThemingField.displayName" + :label="userThemingField.label" + :description="userThemingField.description" + data-admin-theming-setting-disable-user-theming + @update:theming="refreshStyles" /> + <a v-if="!canThemeIcons" + :href="docUrlIcons" + rel="noreferrer noopener"> + <em>{{ t('theming', 'Install the ImageMagick PHP extension with support for SVG images to automatically generate favicons based on the uploaded logo and color.') }}</em> + </a> + </div> + </NcSettingsSection> + <AppMenuSection :default-apps.sync="defaultApps" /> + </section> +</template> + +<script> +import { loadState } from '@nextcloud/initial-state' +import { refreshStyles } from './helpers/refreshStyles.js' + +import NcNoteCard from '@nextcloud/vue/components/NcNoteCard' +import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection' +import CheckboxField from './components/admin/CheckboxField.vue' +import ColorPickerField from './components/admin/ColorPickerField.vue' +import FileInputField from './components/admin/FileInputField.vue' +import TextField from './components/admin/TextField.vue' +import AppMenuSection from './components/admin/AppMenuSection.vue' + +const { + defaultBackgroundURL, + + backgroundMime, + backgroundURL, + backgroundColor, + canThemeIcons, + docUrl, + docUrlIcons, + faviconMime, + isThemable, + legalNoticeUrl, + logoheaderMime, + logoMime, + name, + notThemableErrorMessage, + primaryColor, + privacyPolicyUrl, + slogan, + url, + userThemingDisabled, + defaultApps, +} = loadState('theming', 'adminThemingParameters') + +const textFields = [ + { + name: 'name', + value: name, + defaultValue: 'Nextcloud', + type: 'text', + displayName: t('theming', 'Name'), + placeholder: t('theming', 'Name'), + maxlength: 250, + }, + { + name: 'url', + value: url, + defaultValue: 'https://nextcloud.com', + type: 'url', + displayName: t('theming', 'Web link'), + placeholder: 'https://…', + maxlength: 500, + }, + { + name: 'slogan', + value: slogan, + defaultValue: t('theming', 'a safe home for all your data'), + type: 'text', + displayName: t('theming', 'Slogan'), + placeholder: t('theming', 'Slogan'), + maxlength: 500, + }, +] + +const primaryColorPickerField = { + name: 'primary_color', + value: primaryColor, + defaultValue: '#0082c9', + displayName: t('theming', 'Primary color'), + 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 advancedTextFields = [ + { + name: 'imprintUrl', + value: legalNoticeUrl, + defaultValue: '', + type: 'url', + displayName: t('theming', 'Legal notice link'), + placeholder: 'https://…', + maxlength: 500, + }, + { + name: 'privacyUrl', + value: privacyPolicyUrl, + defaultValue: '', + type: 'url', + displayName: t('theming', 'Privacy policy link'), + placeholder: 'https://…', + maxlength: 500, + }, +] + +const advancedFileInputFields = [ + { + name: 'logoheader', + mimeName: 'logoheaderMime', + mimeValue: logoheaderMime, + defaultMimeValue: '', + displayName: t('theming', 'Header logo'), + ariaLabel: t('theming', 'Upload new header logo'), + }, + { + name: 'favicon', + mimeName: 'faviconMime', + mimeValue: faviconMime, + defaultMimeValue: '', + displayName: t('theming', 'Favicon'), + ariaLabel: t('theming', 'Upload new favicon'), + }, +] + +const userThemingField = { + name: 'disable-user-theming', + value: userThemingDisabled, + defaultValue: false, + displayName: t('theming', 'User settings'), + label: t('theming', 'Disable user theming'), + description: t('theming', 'Although you can select and customize your instance, users can change their background and colors. If you want to enforce your customization, you can toggle this on.'), +} + +export default { + name: 'AdminTheming', + + components: { + AppMenuSection, + CheckboxField, + ColorPickerField, + FileInputField, + NcNoteCard, + NcSettingsSection, + TextField, + }, + + data() { + return { + backgroundMime, + backgroundURL, + backgroundColor, + defaultBackgroundColor: '#0069c3', + + logoMime, + + textFields, + primaryColorPickerField, + advancedTextFields, + advancedFileInputFields, + userThemingField, + defaultApps, + + canThemeIcons, + docUrl, + docUrlIcons, + isThemable, + notThemableErrorMessage, + } + }, + + computed: { + cssBackgroundImage() { + if (this.backgroundURL) { + return `url('${this.backgroundURL}')` + } + return 'unset' + }, + }, + + watch: { + 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) + + 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 + }) + }, + }, +} +</script> + +<style lang="scss" scoped> +.admin-theming, +.admin-theming-advanced { + display: flex; + flex-direction: column; + gap: 8px 0; +} + +.admin-theming { + &__preview { + width: 230px; + height: 140px; + background-size: cover; + background-position: center; + text-align: center; + margin-top: 10px; + background-color: v-bind('backgroundColor'); + background-image: v-bind('cssBackgroundImage'); + + &-logo { + width: 20%; + height: 20%; + margin-top: 20px; + display: inline-block; + background-size: contain; + background-position: center; + background-repeat: no-repeat; + background-image: var(--image-logo, url('../../../core/img/logo/logo.svg')); + } + } +} +</style> |