diff options
Diffstat (limited to 'apps/theming/src/components/BackgroundSettings.vue')
-rw-r--r-- | apps/theming/src/components/BackgroundSettings.vue | 281 |
1 files changed, 133 insertions, 148 deletions
diff --git a/apps/theming/src/components/BackgroundSettings.vue b/apps/theming/src/components/BackgroundSettings.vue index 3629ece40d2..58b76dd9602 100644 --- a/apps/theming/src/components/BackgroundSettings.vue +++ b/apps/theming/src/components/BackgroundSettings.vue @@ -1,46 +1,54 @@ <!-- - - @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - - - - @author Christopher Ng <chrng8@gmail.com> - - @author Greta Doci <gretadoci@gmail.com> - - @author John Molakvoæ <skjnldsv@protonmail.com> - - @author Julius Härtl <jus@bitgrid.net> - - - - @license GNU AGPL version 3 or any later version - - - - This program is free software: you can redistribute it and/or modify - - it under the terms of the GNU Affero General Public License as - - published by the Free Software Foundation, either version 3 of the - - License, or (at your option) any later version. - - - - This program is distributed in the hope that it will be useful, - - but WITHOUT ANY WARRANTY; without even the implied warranty of - - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - - GNU Affero General Public License for more details. - - - - You should have received a copy of the GNU Affero General Public License - - along with this program. If not, see <http://www.gnu.org/licenses/>. - - - --> + - 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 class="background background__filepicker" - :class="{ 'icon-loading': loading === 'custom', 'background--active': backgroundImage === 'custom' }" - :data-color-bright="invertTextColor(Theming.color)" + <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 class="background background__default" - :class="{ 'icon-loading': loading === 'default', 'background--active': backgroundImage === 'default' }" - :data-color-bright="invertTextColor(Theming.defaultColor)" - :style="{ '--border-color': Theming.defaultColor }" + <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"> @@ -48,76 +56,57 @@ <Check :size="44" /> </button> - <!-- Custom color picker --> - <NcColorPicker v-model="Theming.color" @input="debouncePickColor"> - <button class="background background__color" - :data-color="Theming.color" - :data-color-bright="invertTextColor(Theming.color)" - :style="{ backgroundColor: Theming.color, '--border-color': Theming.color}" - data-user-theming-background-color - tabindex="0"> - {{ t('theming', 'Change color') }} - </button> - </NcColorPicker> - <!-- Background set selection --> <button v-for="shippedBackground in shippedBackgrounds" :key="shippedBackground.name" :title="shippedBackground.details.attribution" - :aria-label="shippedBackground.details.attribution" - :class="{ 'icon-loading': loading === shippedBackground.name, 'background--active': backgroundImage === shippedBackground.name }" - :data-color-bright="shippedBackground.details.theming === 'dark'" + :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 }" - class="background background__shipped" tabindex="0" @click="setShipped(shippedBackground.name)"> <Check :size="44" /> </button> - - <!-- Remove background --> - <button class="background background__delete" - data-user-theming-background-clear - tabindex="0" - @click="removeBackground"> - {{ t('theming', 'Remove background') }} - <Close :size="32" /> - </button> </div> </template> <script> -import { generateFilePath, generateRemoteUrl, generateUrl } from '@nextcloud/router' +import { generateFilePath, generateUrl } from '@nextcloud/router' +import { getFilePickerBuilder, showError } from '@nextcloud/dialogs' import { loadState } from '@nextcloud/initial-state' import axios from '@nextcloud/axios' -import Check from 'vue-material-design-icons/Check.vue' -import Close from 'vue-material-design-icons/Close.vue' import debounce from 'debounce' -import NcColorPicker from '@nextcloud/vue/dist/Components/NcColorPicker' -import Vibrant from 'node-vibrant' -import { Palette } from 'node-vibrant/lib/color' -import { getFilePickerBuilder } from '@nextcloud/dialogs' -import { getCurrentUser } from '@nextcloud/auth' +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 backgroundImage = loadState('theming', 'backgroundImage') const shippedBackgroundList = loadState('theming', 'shippedBackgrounds') -const themingDefaultBackground = loadState('theming', 'themingDefaultBackground') -const defaultShippedBackground = loadState('theming', 'defaultShippedBackground') +const backgroundImage = loadState('theming', 'userBackgroundImage') +const { + backgroundImage: defaultBackgroundImage, + // backgroundColor: defaultBackgroundColor, + backgroundMime: defaultBackgroundMime, + defaultShippedBackground, +} = loadState('theming', 'themingDefaults') const prefixWithBaseUrl = (url) => generateFilePath('theming', '', 'img/background/') + url -const picker = getFilePickerBuilder(t('theming', 'Select a background from your files')) - .setMultiSelect(false) - .setModal(true) - .setType(1) - .setMimeTypeFilter(['image/png', 'image/gif', 'image/jpeg', 'image/svg+xml', 'image/svg']) - .build() export default { name: 'BackgroundSettings', components: { Check, - Close, + ColorPalette, + ImageEdit, NcColorPicker, }, @@ -134,7 +123,12 @@ export default { computed: { shippedBackgrounds() { return Object.keys(shippedBackgroundList) - .map(fileName => { + .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), @@ -142,22 +136,18 @@ export default { details: shippedBackgroundList[fileName], } }) - .filter(background => { - // If the admin did not changed the global background - // let's hide the default background to not show it twice - if (!this.isGlobalBackgroundDeleted && !this.isGlobalBackgroundDefault) { - return background.name !== defaultShippedBackground - } - return true - }) }, isGlobalBackgroundDefault() { - return !!themingDefaultBackground + return defaultBackgroundMime === '' }, isGlobalBackgroundDeleted() { - return themingDefaultBackground === 'backgroundColor' + return defaultBackgroundMime === 'backgroundColor' + }, + + cssDefaultBackgroundImage() { + return `url('${defaultBackgroundImage}')` }, }, @@ -205,7 +195,7 @@ export default { async update(data) { // Update state this.backgroundImage = data.backgroundImage - this.Theming.color = data.backgroundColor + this.Theming.backgroundColor = data.backgroundColor // Notify parent and reload style this.$emit('update:background') @@ -224,9 +214,9 @@ export default { this.update(result.data) }, - async setFile(path, color = null) { + async setFile(path) { this.loading = 'custom' - const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path, color }) + const result = await axios.post(generateUrl('/apps/theming/background/custom'), { value: path }) this.update(result.data) }, @@ -236,58 +226,42 @@ export default { this.update(result.data) }, - async pickColor(event) { + async pickColor(color) { this.loading = 'color' - const color = event?.target?.dataset?.color || this.Theming?.color || '#0082c9' - const result = await axios.post(generateUrl('/apps/theming/background/color'), { color }) - this.update(result.data) + const { data } = await axios.post(generateUrl('/apps/theming/background/color'), { color: color || '#0082c9' }) + this.update(data) }, - debouncePickColor: debounce(function() { - this.pickColor(...arguments) - }, 200), - async pickFile() { - const path = await picker.pick() - this.loading = 'custom' + 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() + }, - // Extract primary color from image - let response = null - let color = null - try { - const fileUrl = generateRemoteUrl('dav/files/' + getCurrentUser().uid + path) - response = await axios.get(fileUrl, { responseType: 'blob' }) - const blobUrl = URL.createObjectURL(response.data) - const palette = await this.getColorPaletteFromBlob(blobUrl) - - // DarkVibrant is accessible AND visually pleasing - // Vibrant is not accessible enough and others are boring - color = palette?.DarkVibrant?.hex - this.setFile(path, color) - - // Log data - console.debug('Extracted colour', color, 'from custom image', path, palette) - } catch (error) { - this.setFile(path) - console.error('Unable to extract colour from custom image', { error, path, response, color }) + 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 } - }, - /** - * Extract a Vibrant color palette from a blob URL - * - * @param {string} blobUrl the blob URL - * @return {Promise<Palette>} - */ - getColorPaletteFromBlob(blobUrl) { - return new Promise((resolve, reject) => { - const vibrant = new Vibrant(blobUrl) - vibrant.getPalette((error, palette) => { - if (error) { - reject(error) - } - resolve(palette) - }) - }) + this.loading = 'custom' + this.setFile(path) }, }, } @@ -299,38 +273,48 @@ export default { 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: white; + color: var(--color-background-plain-text); background-image: var(--image-background); } } &__default { - background-color: var(--color-primary-default); - 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); } - &__color { - color: var(--color-primary-text); - background-color: var(--color-primary-default); - } - // Over a background image &__default, &__shipped { @@ -345,8 +329,8 @@ export default { &--active, &:hover, &:focus { - // Use theme color primary, see inline css variable in template - border: 2px solid var(--border-color, var(--color-primary)) !important; + outline: 2px solid var(--color-main-text) !important; + border-color: var(--color-main-background) !important; } // Icon @@ -354,14 +338,15 @@ export default { margin: 4px; } - &__filepicker span, - &__default span, - &__shipped span { + .check-icon { display: none; } - &--active:not(.icon-loading) span { - display: block !important; + &--active:not(.icon-loading) { + .check-icon { + // Show checkmark + display: block !important; + } } } } |