diff options
Diffstat (limited to 'apps/theming/src/components/BackgroundSettings.vue')
-rw-r--r-- | apps/theming/src/components/BackgroundSettings.vue | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/apps/theming/src/components/BackgroundSettings.vue b/apps/theming/src/components/BackgroundSettings.vue new file mode 100644 index 00000000000..58b76dd9602 --- /dev/null +++ b/apps/theming/src/components/BackgroundSettings.vue @@ -0,0 +1,354 @@ +<!-- + - SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="background-selector" data-user-theming-background-settings> + <!-- Custom background --> + <button :aria-pressed="backgroundImage === 'custom'" + :class="{ + 'icon-loading': loading === 'custom', + 'background background__filepicker': true, + 'background--active': backgroundImage === 'custom' + }" + data-user-theming-background-custom + tabindex="0" + @click="pickFile"> + {{ t('theming', 'Custom background') }} + <ImageEdit v-if="backgroundImage !== 'custom'" :size="20" /> + <Check :size="44" /> + </button> + + <!-- Custom color picker --> + <NcColorPicker v-model="Theming.backgroundColor" @update:value="debouncePickColor"> + <button :class="{ + 'icon-loading': loading === 'color', + 'background background__color': true, + 'background--active': backgroundImage === 'color' + }" + :aria-pressed="backgroundImage === 'color'" + :data-color="Theming.backgroundColor" + :data-color-bright="invertTextColor(Theming.backgroundColor)" + :style="{ backgroundColor: Theming.backgroundColor, '--border-color': Theming.backgroundColor}" + data-user-theming-background-color + tabindex="0" + @click="backgroundImage !== 'color' && debouncePickColor(Theming.backgroundColor)"> + {{ t('theming', 'Plain background') /* TRANSLATORS: Background using a single color */ }} + <ColorPalette v-if="backgroundImage !== 'color'" :size="20" /> + <Check :size="44" /> + </button> + </NcColorPicker> + + <!-- Default background --> + <button :aria-pressed="backgroundImage === 'default'" + :class="{ + 'icon-loading': loading === 'default', + 'background background__default': true, + 'background--active': backgroundImage === 'default' + }" + :data-color-bright="invertTextColor(Theming.defaultBackgroundColor)" + :style="{ '--border-color': Theming.defaultBackgroundColor }" + data-user-theming-background-default + tabindex="0" + @click="setDefault"> + {{ t('theming', 'Default background') }} + <Check :size="44" /> + </button> + + <!-- Background set selection --> + <button v-for="shippedBackground in shippedBackgrounds" + :key="shippedBackground.name" + :title="shippedBackground.details.attribution" + :aria-label="shippedBackground.details.description" + :aria-pressed="backgroundImage === shippedBackground.name" + :class="{ + 'background background__shipped': true, + 'icon-loading': loading === shippedBackground.name, + 'background--active': backgroundImage === shippedBackground.name + }" + :data-color-bright="invertTextColor(shippedBackground.details.background_color)" + :data-user-theming-background-shipped="shippedBackground.name" + :style="{ backgroundImage: 'url(' + shippedBackground.preview + ')', '--border-color': shippedBackground.details.primary_color }" + tabindex="0" + @click="setShipped(shippedBackground.name)"> + <Check :size="44" /> + </button> + </div> +</template> + +<script> +import { generateFilePath, generateUrl } from '@nextcloud/router' +import { getFilePickerBuilder, showError } from '@nextcloud/dialogs' +import { loadState } from '@nextcloud/initial-state' +import axios from '@nextcloud/axios' +import debounce from 'debounce' +import NcColorPicker from '@nextcloud/vue/components/NcColorPicker' + +import Check from 'vue-material-design-icons/Check.vue' +import ImageEdit from 'vue-material-design-icons/ImageEdit.vue' +import ColorPalette from 'vue-material-design-icons/PaletteOutline.vue' + +const shippedBackgroundList = loadState('theming', 'shippedBackgrounds') +const backgroundImage = loadState('theming', 'userBackgroundImage') +const { + backgroundImage: defaultBackgroundImage, + // backgroundColor: defaultBackgroundColor, + backgroundMime: defaultBackgroundMime, + defaultShippedBackground, +} = loadState('theming', 'themingDefaults') + +const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url + +export default { + name: 'BackgroundSettings', + + components: { + Check, + ColorPalette, + ImageEdit, + NcColorPicker, + }, + + data() { + return { + loading: false, + Theming: loadState('theming', 'data', {}), + + // User background image and color settings + backgroundImage, + } + }, + + computed: { + shippedBackgrounds() { + return Object.keys(shippedBackgroundList) + .filter((background) => { + // If the admin did not changed the global background + // let's hide the default background to not show it twice + return background !== defaultShippedBackground || !this.isGlobalBackgroundDefault + }) + .map((fileName) => { + return { + name: fileName, + url: prefixWithBaseUrl(fileName), + preview: prefixWithBaseUrl('preview/' + fileName), + details: shippedBackgroundList[fileName], + } + }) + }, + + isGlobalBackgroundDefault() { + return defaultBackgroundMime === '' + }, + + isGlobalBackgroundDeleted() { + return defaultBackgroundMime === 'backgroundColor' + }, + + cssDefaultBackgroundImage() { + return `url('${defaultBackgroundImage}')` + }, + }, + + methods: { + /** + * Do we need to invert the text if color is too bright? + * + * @param {string} color the hex color + */ + invertTextColor(color) { + return this.calculateLuma(color) > 0.6 + }, + + /** + * Calculate luminance of provided hex color + * + * @param {string} color the hex color + */ + calculateLuma(color) { + const [red, green, blue] = this.hexToRGB(color) + return (0.2126 * red + 0.7152 * green + 0.0722 * blue) / 255 + }, + + /** + * Convert hex color to RGB + * + * @param {string} hex the hex color + */ + hexToRGB(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex) + return result + ? [parseInt(result[1], 16), parseInt(result[2], 16), parseInt(result[3], 16)] + : null + }, + + /** + * Update local state + * + * @param {object} data destructuring object + * @param {string} data.backgroundColor background color value + * @param {string} data.backgroundImage background image value + * @param {string} data.version cache buster number + * @see https://github.com/nextcloud/server/blob/c78bd45c64d9695724fc44fe8453a88824b85f2f/apps/theming/lib/Controller/UserThemeController.php#L187-L191 + */ + async update(data) { + // Update state + this.backgroundImage = data.backgroundImage + this.Theming.backgroundColor = data.backgroundColor + + // Notify parent and reload style + this.$emit('update:background') + this.loading = false + }, + + async setDefault() { + this.loading = 'default' + const result = await axios.post(generateUrl('/apps/theming/background/default')) + this.update(result.data) + }, + + async setShipped(shipped) { + this.loading = shipped + const result = await axios.post(generateUrl('/apps/theming/background/shipped'), { value: shipped }) + this.update(result.data) + }, + + async setFile(path) { + this.loading = 'custom' + const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path }) + this.update(result.data) + }, + + async removeBackground() { + this.loading = 'remove' + const result = await axios.delete(generateUrl('/apps/theming/background/custom')) + this.update(result.data) + }, + + async pickColor(color) { + this.loading = 'color' + const { data } = await axios.post(generateUrl('/apps/theming/background/color'), { color: color || '#0082c9' }) + this.update(data) + }, + + debouncePickColor: debounce(function(...args) { + this.pickColor(...args) + }, 1000), + + pickFile() { + const picker = getFilePickerBuilder(t('theming', 'Select a background from your files')) + .allowDirectories(false) + .setMimeTypeFilter(['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml', 'image/svg']) + .setMultiSelect(false) + .addButton({ + id: 'select', + label: t('theming', 'Select background'), + callback: (nodes) => { + this.applyFile(nodes[0]?.path) + }, + type: 'primary', + }) + .build() + picker.pick() + }, + + async applyFile(path) { + if (!path || typeof path !== 'string' || path.trim().length === 0 || path === '/') { + console.error('No valid background have been selected', { path }) + showError(t('theming', 'No background has been selected')) + return + } + + this.loading = 'custom' + this.setFile(path) + }, + }, +} +</script> + +<style scoped lang="scss"> +.background-selector { + display: flex; + flex-wrap: wrap; + justify-content: center; + + .background-color { + display: flex; + justify-content: center; + align-items: center; + width: 176px; + height: 96px; + margin: 8px; + border-radius: var(--border-radius-large); + background-color: var(--color-primary); + } + + .background { + overflow: hidden; + width: 176px; + height: 96px; + margin: 8px; + text-align: center; + word-wrap: break-word; + hyphens: auto; + border: 2px solid var(--color-main-background); + border-radius: var(--border-radius-large); + background-position: center center; + background-size: cover; + + &__filepicker { + background-color: var(--color-background-dark); + + &.background--active { + color: var(--color-background-plain-text); + background-image: var(--image-background); + } + } + + &__default { + background-color: var(--color-background-plain); + background-image: linear-gradient(to bottom, rgba(23, 23, 23, 0.5), rgba(23, 23, 23, 0.5)), v-bind(cssDefaultBackgroundImage); + } + + &__filepicker, &__default, &__color { + border-color: var(--color-border); + } + + // Over a background image + &__default, + &__shipped { + color: white; + } + + // Text and svg icon dark on bright background + &[data-color-bright] { + color: black; + } + + &--active, + &:hover, + &:focus { + outline: 2px solid var(--color-main-text) !important; + border-color: var(--color-main-background) !important; + } + + // Icon + span { + margin: 4px; + } + + .check-icon { + display: none; + } + + &--active:not(.icon-loading) { + .check-icon { + // Show checkmark + display: block !important; + } + } + } +} + +</style> |