aboutsummaryrefslogtreecommitdiffstats
path: root/apps/theming/src/AdminTheming.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/theming/src/AdminTheming.vue')
-rw-r--r--apps/theming/src/AdminTheming.vue365
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>