diff options
Diffstat (limited to 'apps/dashboard/src')
-rw-r--r-- | apps/dashboard/src/DashboardApp.vue | 244 | ||||
-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/main.js | 37 | ||||
-rw-r--r-- | apps/dashboard/src/mixins/isMobile.js | 21 |
5 files changed, 374 insertions, 112 deletions
diff --git a/apps/dashboard/src/DashboardApp.vue b/apps/dashboard/src/DashboardApp.vue index 607e677fa7f..afc874be2c9 100644 --- a/apps/dashboard/src/DashboardApp.vue +++ b/apps/dashboard/src/DashboardApp.vue @@ -1,12 +1,16 @@ +<!-- + - SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later + --> <template> - <div id="app-dashboard"> + <main id="app-dashboard"> <h2>{{ greeting.text }}</h2> <ul class="statuses"> - <div v-for="status in sortedRegisteredStatus" + <li v-for="status in sortedRegisteredStatus" :id="'status-' + status" :key="status"> <div :ref="'status-' + status" /> - </div> + </li> </ul> <Draggable v-model="layout" @@ -14,17 +18,35 @@ v-bind="{swapThreshold: 0.30, delay: 500, delayOnTouchOnly: true, touchStartThreshold: 3}" handle=".panel--header" @end="saveLayout"> - <div v-for="panelId in layout" :key="panels[panelId].id" class="panel"> - <div class="panel--header"> - <h2> - <div :class="panels[panelId].iconClass" role="img" /> - {{ panels[panelId].title }} - </h2> + <template v-for="panelId in layout"> + <div v-if="isApiWidgetV2(panels[panelId].id)" + :key="`${panels[panelId].id}-v2`" + class="panel"> + <div class="panel--header"> + <h2> + <img v-if="apiWidgets[panels[panelId].id].icon_url" :src="apiWidgets[panels[panelId].id].icon_url" alt=""> + <span v-else :class="apiWidgets[panels[panelId].id].icon_class" aria-hidden="true" /> + {{ apiWidgets[panels[panelId].id].title }} + </h2> + </div> + <div class="panel--content"> + <ApiDashboardWidget :widget="apiWidgets[panels[panelId].id]" + :data="apiWidgetItems[panels[panelId].id]" + :loading="loadingItems" /> + </div> </div> - <div class="panel--content" :class="{ loading: !panels[panelId].mounted }"> - <div :ref="panels[panelId].id" :data-id="panels[panelId].id" /> + <div v-else :key="panels[panelId].id" class="panel"> + <div class="panel--header"> + <h2> + <span :class="panels[panelId].iconClass" aria-hidden="true" /> + {{ panels[panelId].title }} + </h2> + </div> + <div class="panel--content" :class="{ loading: !panels[panelId].mounted }"> + <div :ref="panels[panelId].id" :data-id="panels[panelId].id" /> + </div> </div> - </div> + </template> </Draggable> <div class="footer"> @@ -38,7 +60,7 @@ <NcModal v-if="modal" size="large" @close="closeModal"> <div class="modal__content"> - <h3>{{ t('dashboard', 'Edit widgets') }}</h3> + <h2>{{ t('dashboard', 'Edit widgets') }}</h2> <ol class="panels"> <li v-for="status in sortedAllStatuses" :key="status" :class="'panel-' + status"> <input :id="'status-checkbox-' + status" @@ -47,7 +69,8 @@ :checked="isStatusActive(status)" @input="updateStatusCheckbox(status, $event.target.checked)"> <label :for="'status-checkbox-' + status"> - <div :class="statusInfo[status].icon" role="img" /> + <NcUserStatusIcon v-if="status === 'status'" status="online" aria-hidden="true" /> + <span v-else :class="statusInfo[status].icon" aria-hidden="true" /> {{ statusInfo[status].text }} </label> </li> @@ -65,43 +88,49 @@ :checked="isActive(panel)" @input="updateCheckbox(panel, $event.target.checked)"> <label :for="'panel-checkbox-' + panel.id" :class="{ draggable: isActive(panel) }"> - <div :class="panel.iconClass" role="img" /> + <img v-if="panel.iconUrl" alt="" :src="panel.iconUrl"> + <span v-else :class="panel.iconClass" aria-hidden="true" /> {{ panel.title }} </label> </li> </Draggable> - <a v-if="isAdmin" :href="appStoreUrl" class="button">{{ t('dashboard', 'Get more widgets from the App Store') }}</a> - - <h3>{{ t('dashboard', 'Weather service') }}</h3> - <p> - {{ t('dashboard', 'For your privacy, the weather data is requested by your Nextcloud server on your behalf so the weather service receives no personal information.') }} - </p> - <p class="credits--end"> - <a href="https://api.met.no/doc/TermsOfService" target="_blank" rel="noopener">{{ t('dashboard', 'Weather data from Met.no') }}</a>, - <a href="https://wiki.osmfoundation.org/wiki/Privacy_Policy" target="_blank" rel="noopener">{{ t('dashboard', 'geocoding with Nominatim') }}</a>, - <a href="https://www.opentopodata.org/#public-api" target="_blank" rel="noopener">{{ t('dashboard', 'elevation data from OpenTopoData') }}</a>. - </p> + <a v-if="isAdmin && appStoreEnabled" :href="appStoreUrl" class="button">{{ t('dashboard', 'Get more widgets from the App Store') }}</a> + + <div v-if="statuses.weather && isStatusActive('weather')"> + <h2>{{ t('dashboard', 'Weather service') }}</h2> + <p> + {{ t('dashboard', 'For your privacy, the weather data is requested by your Nextcloud server on your behalf so the weather service receives no personal information.') }} + </p> + <p class="credits--end"> + <a href="https://api.met.no/doc/TermsOfService" target="_blank" rel="noopener">{{ t('dashboard', 'Weather data from Met.no') }}</a>, + <a href="https://wiki.osmfoundation.org/wiki/Privacy_Policy" target="_blank" rel="noopener">{{ t('dashboard', 'geocoding with Nominatim') }}</a>, + <a href="https://www.opentopodata.org/#public-api" target="_blank" rel="noopener">{{ t('dashboard', 'elevation data from OpenTopoData') }}</a>. + </p> + </div> </div> </NcModal> - </div> + </main> </template> <script> -import { generateUrl } from '@nextcloud/router' +import { generateUrl, generateOcsUrl } from '@nextcloud/router' import { getCurrentUser } from '@nextcloud/auth' import { loadState } from '@nextcloud/initial-state' import axios from '@nextcloud/axios' -import NcButton from '@nextcloud/vue/dist/Components/NcButton' +import NcButton from '@nextcloud/vue/components/NcButton' import Draggable from 'vuedraggable' -import NcModal from '@nextcloud/vue/dist/Components/NcModal' +import NcModal from '@nextcloud/vue/components/NcModal' +import NcUserStatusIcon from '@nextcloud/vue/components/NcUserStatusIcon' import Pencil from 'vue-material-design-icons/Pencil.vue' import Vue from 'vue' import isMobile from './mixins/isMobile.js' +import ApiDashboardWidget from './components/ApiDashboardWidget.vue' const panels = loadState('dashboard', 'panels') const firstRun = loadState('dashboard', 'firstRun') +const birthdate = new Date(loadState('dashboard', 'birthdate')) const statusInfo = { weather: { @@ -110,17 +139,18 @@ const statusInfo = { }, status: { text: t('dashboard', 'Status'), - icon: 'icon-user-status-online', }, } export default { name: 'DashboardApp', components: { + ApiDashboardWidget, NcButton, Draggable, NcModal, Pencil, + NcUserStatusIcon, }, mixins: [ isMobile, @@ -143,16 +173,26 @@ export default { layout: loadState('dashboard', 'layout').filter((panelId) => panels[panelId]), modal: false, appStoreUrl: generateUrl('/settings/apps/dashboard'), + appStoreEnabled: loadState('dashboard', 'appStoreEnabled', true), statuses: {}, + apiWidgets: [], + apiWidgetItems: {}, + loadingItems: true, + birthdate, } }, computed: { greeting() { const time = this.timer.getHours() + const isBirthday = this.birthdate instanceof Date + && this.birthdate.getMonth() === this.timer.getMonth() + && this.birthdate.getDate() === this.timer.getDate() // Determine part of the day let partOfDay - if (time >= 22 || time < 5) { + if (isBirthday) { + partOfDay = 'birthday' + } else if (time >= 22 || time < 5) { partOfDay = 'night' } else if (time >= 18) { partOfDay = 'evening' @@ -181,6 +221,10 @@ export default { generic: t('dashboard', 'Hello'), withName: t('dashboard', 'Hello, {name}', { name: this.displayName }, undefined, { escape: false }), }, + birthday: { + generic: t('dashboard', 'Happy birthday 🥳🤩🎂🎉'), + withName: t('dashboard', 'Happy birthday, {name} 🥳🤩🎂🎉', { name: this.displayName }, undefined, { escape: false }), + }, } // Figure out which greeting to show @@ -192,7 +236,7 @@ export default { return (panel) => this.layout.indexOf(panel.id) > -1 }, isStatusActive() { - return (status) => !(status in this.enabledStatuses) || this.enabledStatuses[status] + return (status) => this.enabledStatuses.findIndex((s) => s === status) !== -1 }, sortedAllStatuses() { @@ -233,6 +277,27 @@ export default { }, }, + async created() { + await this.fetchApiWidgets() + + const apiWidgetIdsToFetch = Object + .values(this.apiWidgets) + .filter(widget => this.isApiWidgetV2(widget.id) && this.layout.includes(widget.id)) + .map(widget => widget.id) + await Promise.all(apiWidgetIdsToFetch.map(id => this.fetchApiWidgetItems([id], true))) + + for (const widget of Object.values(this.apiWidgets)) { + if (widget.reload_interval > 0) { + setInterval(async () => { + if (!this.layout.includes(widget.id)) { + return + } + + await this.fetchApiWidgetItems([widget.id], true) + }, widget.reload_interval * 1000) + } + } + }, mounted() { this.updateSkipLink() window.addEventListener('scroll', this.handleScroll) @@ -272,6 +337,11 @@ export default { }, rerenderPanels() { for (const app in this.callbacks) { + // TODO: Properly rerender v2 widgets + if (this.isApiWidgetV2(this.panels[app].id)) { + continue + } + const element = this.$refs[app] if (this.layout.indexOf(app) === -1) { continue @@ -290,13 +360,13 @@ export default { } }, saveLayout() { - axios.post(generateUrl('/apps/dashboard/layout'), { - layout: this.layout.join(','), + axios.post(generateOcsUrl('/apps/dashboard/api/v3/layout'), { + layout: this.layout, }) }, saveStatuses() { - axios.post(generateUrl('/apps/dashboard/statuses'), { - statuses: JSON.stringify(this.enabledStatuses), + axios.post(generateOcsUrl('/apps/dashboard/api/v3/statuses'), { + statuses: this.enabledStatuses, }) }, showModal() { @@ -310,9 +380,11 @@ export default { const index = this.layout.indexOf(panel.id) if (!currentValue && index > -1) { this.layout.splice(index, 1) - } else { this.layout.push(panel.id) + if (this.isApiWidgetV2(panel.id)) { + this.fetchApiWidgetItems([panel.id], true) + } } Vue.set(this.panels[panel.id], 'mounted', false) this.saveLayout() @@ -336,15 +408,18 @@ export default { } }, enableStatus(app) { - this.enabledStatuses[app] = true + this.enabledStatuses.push(app) this.registerStatus(app, this.allCallbacksStatus[app]) this.saveStatuses() }, disableStatus(app) { - this.enabledStatuses[app] = false - const i = this.registeredStatus.findIndex((s) => s === app) + const i = this.enabledStatuses.findIndex((s) => s === app) if (i !== -1) { - this.registeredStatus.splice(i, 1) + this.enabledStatuses.splice(i, 1) + } + const j = this.registeredStatus.findIndex((s) => s === app) + if (j !== -1) { + this.registeredStatus.splice(j, 1) Vue.set(this.statuses, app, { mounted: false }) this.$nextTick(() => { Vue.delete(this.callbacksStatus, app) @@ -368,6 +443,33 @@ export default { document.body.classList.remove('dashboard--scrolled') } }, + async fetchApiWidgets() { + const { data } = await axios.get(generateOcsUrl('/apps/dashboard/api/v1/widgets')) + this.apiWidgets = data.ocs.data + }, + async fetchApiWidgetItems(widgetIds, merge = false) { + try { + const url = generateOcsUrl('/apps/dashboard/api/v2/widget-items') + const params = new URLSearchParams(widgetIds.map(id => ['widgets[]', id])) + const response = await axios.get(`${url}?${params.toString()}`) + const widgetItems = response.data.ocs.data + if (merge) { + this.apiWidgetItems = Object.assign({}, this.apiWidgetItems, widgetItems) + } else { + this.apiWidgetItems = widgetItems + } + } finally { + this.loadingItems = false + } + }, + isApiWidgetV2(id) { + for (const widget of Object.values(this.apiWidgets)) { + if (widget.id === id && widget.item_api_versions.includes(2)) { + return true + } + } + return false + }, }, } </script> @@ -382,7 +484,8 @@ export default { background-attachment: fixed; > h2 { - color: var(--color-primary-text); + // this is shown directly on the background image / color + color: var(--color-background-plain-text); text-align: center; font-size: 32px; line-height: 130%; @@ -402,6 +505,8 @@ export default { } .panel, .panels > div { + // Ensure the maxcontrast color is set for the background + --color-text-maxcontrast: var(--color-text-maxcontrast-background-blur, var(--color-main-text)); width: 320px; max-width: 100%; margin: 16px; @@ -409,7 +514,7 @@ export default { background-color: var(--color-main-background-blur); -webkit-backdrop-filter: var(--filter-background-blur); backdrop-filter: var(--filter-background-blur); - border-radius: var(--border-radius-large); + border-radius: var(--border-radius-container-large); #body-user.theme--highcontrast & { border: 2px solid var(--color-border); @@ -426,7 +531,8 @@ export default { padding: 16px; cursor: grab; - &, ::v-deep * { + &, + :deep(*) { -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -457,13 +563,20 @@ export default { overflow: hidden; text-overflow: ellipsis; cursor: grab; - div { + + img, + span { background-size: 32px; width: 32px; height: 32px; - margin-right: 16px; background-position: center; float: left; + margin-top: -6px; + margin-inline: 6px 16px; + } + + img { + filter: var(--background-invert-if-dark); } } } @@ -495,7 +608,7 @@ export default { margin:auto; background-position: 16px center; padding: 12px 16px; - padding-left: 36px; + padding-inline-start: 36px; border-radius: var(--border-radius-pill); max-width: 200px; opacity: 1; @@ -505,8 +618,10 @@ export default { .button, .button-vue, .edit-panels, -.statuses ::v-deep .action-item .action-item__menutoggle, -.statuses ::v-deep .action-item.action-item--open .action-item__menutoggle { +.statuses :deep(.action-item .action-item__menutoggle), +.statuses :deep(.action-item.action-item--open .action-item__menutoggle) { + // Ensure the maxcontrast color is set for the background + --color-text-maxcontrast: var(--color-text-maxcontrast-background-blur, var(--color-main-text)); background-color: var(--color-main-background-blur); -webkit-backdrop-filter: var(--filter-background-blur); backdrop-filter: var(--filter-background-blur); @@ -518,7 +633,8 @@ export default { background-color: var(--color-background-hover)!important; } &:focus-visible { - box-shadow: 0 0 0 2px var(--color-main-text) !important; + box-shadow: 0 0 0 4px var(--color-main-background) !important; + outline: 2px solid var(--color-main-text) !important; } } @@ -543,12 +659,13 @@ export default { background-color: var(--color-background-hover); border: 2px solid var(--color-main-background); border-radius: var(--border-radius-large); - text-align: left; + text-align: start; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - div { + img, + span { position: absolute; top: 16px; width: 24px; @@ -556,33 +673,34 @@ export default { background-size: 24px; } + img { + filter: var(--background-invert-if-dark); + } + &:hover { - border-color: var(--color-primary); + border-color: var(--color-primary-element); } } // Do not invert status icons - &:not(.panel-status) label div { + &:not(.panel-status) label span { filter: var(--background-invert-if-dark); } input[type='checkbox'].checkbox + label:before { position: absolute; - right: 12px; + inset-inline-end: 12px; top: 16px; } input:focus + label { - border-color: var(--color-primary); + border-color: var(--color-primary-element); } } - h3 { + h2 { font-weight: bold; - - &:not(:first-of-type) { - margin-top: 64px; - } + margin-top: 12px; } // Adjust design of 'Get more widgets' button @@ -623,7 +741,7 @@ export default { flex-wrap: wrap; margin-bottom: 36px; - & > div { + & > li { margin: 8px; } } 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/main.js b/apps/dashboard/src/main.js index dc276959b11..68d896a3a17 100644 --- a/apps/dashboard/src/main.js +++ b/apps/dashboard/src/main.js @@ -1,45 +1,22 @@ /** - * @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license AGPL-3.0-or-later - * - * 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: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ +import { getCSPNonce } from '@nextcloud/auth' +import { t } from '@nextcloud/l10n' +import VTooltip from '@nextcloud/vue/directives/Tooltip' import Vue from 'vue' + import DashboardApp from './DashboardApp.vue' -import { translate as t } from '@nextcloud/l10n' -import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip' -import { getRequestToken } from '@nextcloud/auth' // eslint-disable-next-line camelcase -__webpack_nonce__ = btoa(getRequestToken()) +__webpack_nonce__ = getCSPNonce() Vue.directive('Tooltip', VTooltip) Vue.prototype.t = t -// FIXME workaround to make the sidebar work -if (!window.OCA.Files) { - window.OCA.Files = {} -} - -Object.assign(window.OCA.Files, { App: { fileList: { filesClient: OC.Files.getClient() } } }, window.OCA.Files) - const Dashboard = Vue.extend(DashboardApp) const Instance = new Dashboard({}).$mount('#app-content-vue') diff --git a/apps/dashboard/src/mixins/isMobile.js b/apps/dashboard/src/mixins/isMobile.js index 6bae7219fe6..d4062f8c7e0 100644 --- a/apps/dashboard/src/mixins/isMobile.js +++ b/apps/dashboard/src/mixins/isMobile.js @@ -1,23 +1,6 @@ /** - * @copyright Copyright (c) 2020 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * - * @license AGPL-3.0-or-later - * - * 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 */ export default { |