diff options
Diffstat (limited to 'apps/dashboard/src/components')
-rw-r--r-- | apps/dashboard/src/components/ApiDashboardWidget.vue | 116 | ||||
-rw-r--r-- | apps/dashboard/src/components/ApiDashboardWidgetItem.vue | 68 | ||||
-rw-r--r-- | apps/dashboard/src/components/BackgroundSettings.vue | 191 |
3 files changed, 184 insertions, 191 deletions
diff --git a/apps/dashboard/src/components/ApiDashboardWidget.vue b/apps/dashboard/src/components/ApiDashboardWidget.vue new file mode 100644 index 00000000000..4aa8628fac8 --- /dev/null +++ b/apps/dashboard/src/components/ApiDashboardWidget.vue @@ -0,0 +1,116 @@ +<!-- + - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<template> + <NcDashboardWidget :items="items" + :show-more-label="showMoreLabel" + :show-more-url="showMoreUrl" + :loading="loading" + :show-items-and-empty-content="!!halfEmptyContentMessage" + :half-empty-content-message="halfEmptyContentMessage"> + <template #default="{ item }"> + <ApiDashboardWidgetItem :item="item" :icon-size="iconSize" :rounded-icons="widget.item_icons_round" /> + </template> + <template #empty-content> + <NcEmptyContent v-if="items.length === 0" + :description="emptyContentMessage"> + <template #icon> + <CheckIcon v-if="emptyContentMessage" :size="65" /> + </template> + <template #action> + <NcButton v-if="setupButton" :href="setupButton.link"> + {{ setupButton.text }} + </NcButton> + </template> + </NcEmptyContent> + </template> + </NcDashboardWidget> +</template> + +<script> +import NcButton from '@nextcloud/vue/components/NcButton' +import NcDashboardWidget from '@nextcloud/vue/components/NcDashboardWidget' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import CheckIcon from 'vue-material-design-icons/Check.vue' +import ApiDashboardWidgetItem from './ApiDashboardWidgetItem.vue' + +export default { + name: 'ApiDashboardWidget', + components: { + ApiDashboardWidgetItem, + CheckIcon, + NcDashboardWidget, + NcEmptyContent, + NcButton, + }, + props: { + widget: { + type: [Object, undefined], + default: undefined, + }, + data: { + type: [Object, undefined], + default: undefined, + }, + loading: { + type: Boolean, + required: true, + }, + }, + data() { + return { + iconSize: 44, + } + }, + computed: { + /** @return {object[]} */ + items() { + return this.data?.items ?? [] + }, + + /** @return {string} */ + emptyContentMessage() { + return this.data?.emptyContentMessage ?? '' + }, + + /** @return {string} */ + halfEmptyContentMessage() { + return this.data?.halfEmptyContentMessage ?? '' + }, + + /** @return {object|undefined} */ + newButton() { + // TODO: Render new button in the template + // I couldn't find a widget that makes use of the button. Furthermore, there is no convenient + // way to render such a button using the official widget component. + return this.widget?.buttons?.find(button => button.type === 'new') + }, + + /** @return {object|undefined} */ + moreButton() { + return this.widget?.buttons?.find(button => button.type === 'more') + }, + + /** @return {object|undefined} */ + setupButton() { + return this.widget?.buttons?.find(button => button.type === 'setup') + }, + + /** @return {string|undefined} */ + showMoreLabel() { + return this.moreButton?.text + }, + + /** @return {string|undefined} */ + showMoreUrl() { + return this.moreButton?.link + }, + }, + mounted() { + const size = window.getComputedStyle(document.body).getPropertyValue('--default-clickable-area') + const numeric = Number.parseFloat(size) + this.iconSize = Number.isNaN(numeric) ? 44 : numeric + }, +} +</script> diff --git a/apps/dashboard/src/components/ApiDashboardWidgetItem.vue b/apps/dashboard/src/components/ApiDashboardWidgetItem.vue new file mode 100644 index 00000000000..2caa7868fb3 --- /dev/null +++ b/apps/dashboard/src/components/ApiDashboardWidgetItem.vue @@ -0,0 +1,68 @@ +<!-- + - SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> +<script setup lang="ts"> +import { ref } from 'vue' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcDashboardWidgetItem from '@nextcloud/vue/components/NcDashboardWidgetItem' +import IconFile from 'vue-material-design-icons/File.vue' + +defineProps({ + item: { + type: Object, + required: true, + }, + iconSize: { + type: Number, + required: true, + }, + roundedIcons: { + type: Boolean, + default: true, + }, +}) + +/** + * True as soon as the image is loaded + */ +const imageLoaded = ref(false) +/** + * True if the image failed to load and we should show a fallback + */ +const loadingImageFailed = ref(false) +</script> + +<template> + <NcDashboardWidgetItem :target-url="item.link" + :overlay-icon-url="item.overlayIconUrl ? item.overlayIconUrl : ''" + :main-text="item.title" + :sub-text="item.subtitle"> + <template #avatar> + <template v-if="item.iconUrl"> + <NcAvatar v-if="roundedIcons" + :size="iconSize" + :url="item.iconUrl" /> + <template v-else> + <img v-show="!loadingImageFailed" + alt="" + class="api-dashboard-widget-item__icon" + :class="{'hidden-visually': !imageLoaded }" + :src="item.iconUrl" + @error="loadingImageFailed = true" + @load="imageLoaded = true"> + <!-- Placeholder while the image is loaded and also the fallback if the URL is broken --> + <IconFile v-if="!imageLoaded" + :size="iconSize" /> + </template> + </template> + </template> + </NcDashboardWidgetItem> +</template> + +<style scoped> +.api-dashboard-widget-item__icon { + height: var(--default-clickable-area); + width: var(--default-clickable-area); +} +</style> diff --git a/apps/dashboard/src/components/BackgroundSettings.vue b/apps/dashboard/src/components/BackgroundSettings.vue deleted file mode 100644 index 691ce12cfd3..00000000000 --- a/apps/dashboard/src/components/BackgroundSettings.vue +++ /dev/null @@ -1,191 +0,0 @@ -<!-- - - @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - - - - @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/>. - - - --> - -<template> - <div class="background-selector"> - <button class="background filepicker" - :class="{ active: background === 'custom' }" - tabindex="0" - @click="pickFile"> - {{ t('dashboard', 'Pick from Files') }} - </button> - <button class="background default" - tabindex="0" - :class="{ 'icon-loading': loading === 'default', active: background === 'default' }" - @click="setDefault"> - {{ t('dashboard', 'Default images') }} - </button> - <button class="background color" - :class="{ active: background === 'custom' }" - tabindex="0" - @click="pickColor"> - {{ t('dashboard', 'Plain background') }} - </button> - <button v-for="shippedBackground in shippedBackgrounds" - :key="shippedBackground.name" - v-tooltip="shippedBackground.details.attribution" - :class="{ 'icon-loading': loading === shippedBackground.name, active: background === shippedBackground.name }" - tabindex="0" - class="background" - :style="{ 'background-image': 'url(' + shippedBackground.preview + ')' }" - @click="setShipped(shippedBackground.name)" /> - </div> -</template> - -<script> -import axios from '@nextcloud/axios' -import { generateUrl } from '@nextcloud/router' -import { loadState } from '@nextcloud/initial-state' -import getBackgroundUrl from './../helpers/getBackgroundUrl' -import prefixWithBaseUrl from './../helpers/prefixWithBaseUrl' -const shippedBackgroundList = loadState('dashboard', 'shippedBackgrounds') - -export default { - name: 'BackgroundSettings', - props: { - background: { - type: String, - default: 'default', - }, - themingDefaultBackground: { - type: String, - default: '', - }, - }, - data() { - return { - backgroundImage: generateUrl('/apps/dashboard/background') + '?v=' + Date.now(), - loading: false, - } - }, - computed: { - shippedBackgrounds() { - return Object.keys(shippedBackgroundList).map((item) => { - return { - name: item, - url: prefixWithBaseUrl(item), - preview: prefixWithBaseUrl('previews/' + item), - details: shippedBackgroundList[item], - } - }) - }, - }, - methods: { - async update(data) { - const background = data.type === 'custom' || data.type === 'default' ? data.type : data.value - this.backgroundImage = getBackgroundUrl(background, data.version, this.themingDefaultBackground) - if (data.type === 'color' || (data.type === 'default' && this.themingDefaultBackground === 'backgroundColor')) { - this.$emit('update:background', data) - this.loading = false - return - } - const image = new Image() - image.onload = () => { - this.$emit('update:background', data) - this.loading = false - } - image.src = this.backgroundImage - }, - async setDefault() { - this.loading = 'default' - const result = await axios.post(generateUrl('/apps/dashboard/background/default')) - this.update(result.data) - }, - async setShipped(shipped) { - this.loading = shipped - const result = await axios.post(generateUrl('/apps/dashboard/background/shipped'), { value: shipped }) - this.update(result.data) - }, - async setFile(path) { - this.loading = 'custom' - const result = await axios.post(generateUrl('/apps/dashboard/background/custom'), { value: path }) - this.update(result.data) - }, - async pickColor() { - this.loading = 'color' - const color = OCA && OCA.Theming ? OCA.Theming.color : '#0082c9' - const result = await axios.post(generateUrl('/apps/dashboard/background/color'), { value: color }) - this.update(result.data) - }, - pickFile() { - window.OC.dialogs.filepicker(t('dashboard', 'Insert from {productName}', { productName: OC.theme.name }), (path, type) => { - if (type === OC.dialogs.FILEPICKER_TYPE_CHOOSE) { - this.setFile(path) - } - }, false, ['image/png', 'image/gif', 'image/jpeg', 'image/svg'], true, OC.dialogs.FILEPICKER_TYPE_CHOOSE) - }, - }, -} -</script> - -<style scoped lang="scss"> -.background-selector { - display: flex; - flex-wrap: wrap; - justify-content: center; - - .background { - width: 176px; - height: 96px; - margin: 8px; - background-size: cover; - background-position: center center; - text-align: center; - border-radius: var(--border-radius-large); - border: 2px solid var(--color-main-background); - overflow: hidden; - - &.current { - background-image: var(--color-background-dark); - } - - &.filepicker, &.default, &.color { - border-color: var(--color-border); - } - - &.color { - background-color: var(--color-primary); - color: var(--color-primary-text); - } - - &.active, - &:hover, - &:focus { - border: 2px solid var(--color-primary); - } - - &.active:not(.icon-loading):after { - background-image: var(--icon-checkmark-fff); - background-repeat: no-repeat; - background-position: center; - background-size: 44px; - content: ''; - display: block; - height: 100%; - - body.theme--dark & { - background-image: var(--icon-checkmark-000); - } - } - } -} -</style> |