aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dashboard/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dashboard/src/components')
-rw-r--r--apps/dashboard/src/components/ApiDashboardWidget.vue116
-rw-r--r--apps/dashboard/src/components/ApiDashboardWidgetItem.vue68
-rw-r--r--apps/dashboard/src/components/BackgroundSettings.vue191
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>