123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365 |
- <!--
- - 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/dist/Components/NcNoteCard.js'
- import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
- 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>
|