|
@@ -0,0 +1,194 @@
|
|
|
+<!--
|
|
|
+ - @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
|
|
+ -
|
|
|
+ - @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at>
|
|
|
+ -
|
|
|
+ - @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="update">
|
|
|
+ <h2>{{ t('core', 'Recommended apps') }}</h2>
|
|
|
+ <p v-if="loadingApps" class="loading">
|
|
|
+ {{ t('core', 'Loading apps …') }}
|
|
|
+ </p>
|
|
|
+ <p v-else-if="loadingAppsError" class="loading-error">
|
|
|
+ {{ t('core', 'Could not fetch list of apps from the app store.') }}
|
|
|
+ </p>
|
|
|
+ <p v-else>
|
|
|
+ {{ t('core', 'Installing recommended apps …') }}
|
|
|
+ </p>
|
|
|
+ <div v-for="app in recommendedApps" :key="app.id" class="app">
|
|
|
+ <img :src="customIcon(app.id)" :alt="t('core', 'Nextcloud app {app}', { app: app.name })">
|
|
|
+ <div class="info">
|
|
|
+ <h3>
|
|
|
+ {{ app.name }}
|
|
|
+ <span v-if="app.loading" class="icon icon-loading-small" />
|
|
|
+ <span v-else-if="app.active" class="icon icon-checkmark-white" />
|
|
|
+ </h3>
|
|
|
+ <p v-html="customDescription(app.id)" />
|
|
|
+ <p v-if="app.installationError" class="error">
|
|
|
+ {{ t('core', 'App download or installation failed') }}
|
|
|
+ </p>
|
|
|
+ <p v-else-if="!app.isCompatible" class="error">
|
|
|
+ {{ t('core', 'Can\'t install this app because it is not compatible') }}
|
|
|
+ </p>
|
|
|
+ <p v-else-if="!app.canInstall" class="error">
|
|
|
+ {{ t('core', 'Can\'t install this app') }}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <a :href="defaultPageUrl">{{ t('core', 'Go back') }}</a>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import axios from '@nextcloud/axios'
|
|
|
+import { generateUrl, imagePath } from '@nextcloud/router'
|
|
|
+import { loadState } from '@nextcloud/initial-state'
|
|
|
+import pLimit from 'p-limit'
|
|
|
+import { translate as t } from '@nextcloud/l10n'
|
|
|
+
|
|
|
+import logger from '../../logger'
|
|
|
+
|
|
|
+const recommended = {
|
|
|
+ calendar: {
|
|
|
+ description: t('core', 'Schedule work & meetings, synced with all your devices.'),
|
|
|
+ icon: imagePath('core', 'places/calendar.svg')
|
|
|
+ },
|
|
|
+ contacts: {
|
|
|
+ description: t('core', 'Keep your colleagues and friends in one place without leaking their private info.'),
|
|
|
+ icon: imagePath('core', 'places/contacts.svg')
|
|
|
+ },
|
|
|
+ mail: {
|
|
|
+ description: t('core', 'Simple email app nicely integrated with Files, Contacts and Calendar.'),
|
|
|
+ icon: imagePath('core', 'actions/mail.svg')
|
|
|
+ },
|
|
|
+ talk: {
|
|
|
+ description: t('core', 'Screensharing, online meetings and web conferencing – on desktop and with mobile apps.')
|
|
|
+ }
|
|
|
+}
|
|
|
+const recommendedIds = Object.keys(recommended)
|
|
|
+const defaultPageUrl = loadState('core', 'defaultPageUrl')
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'RecommendedApps',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ loadingApps: true,
|
|
|
+ loadingAppsError: false,
|
|
|
+ apps: [],
|
|
|
+ defaultPageUrl
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ recommendedApps() {
|
|
|
+ return this.apps.filter(app => recommendedIds.includes(app.id))
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ return axios.get(generateUrl('settings/apps/list'))
|
|
|
+ .then(resp => resp.data)
|
|
|
+ .then(data => {
|
|
|
+ logger.info(`${data.apps.length} apps fetched`)
|
|
|
+
|
|
|
+ this.apps = data.apps.map(app => Object.assign(app, { loading: false, installationError: false }))
|
|
|
+ logger.debug(`${this.recommendedApps.length} recommended apps found`, { apps: this.recommendedApps })
|
|
|
+
|
|
|
+ this.installApps()
|
|
|
+ })
|
|
|
+ .catch(error => {
|
|
|
+ logger.error('could not fetch app list', { error })
|
|
|
+
|
|
|
+ this.loadingAppsError = true
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ this.loadingApps = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ installApps() {
|
|
|
+ const limit = pLimit(1)
|
|
|
+ const installing = this.recommendedApps
|
|
|
+ .filter(app => !app.active && app.isCompatible && app.canInstall)
|
|
|
+ .map(app => limit(() => {
|
|
|
+ logger.info(`installing ${app.id}`)
|
|
|
+ app.loading = true
|
|
|
+ return axios.post(generateUrl(`settings/apps/enable`), { appIds: [app.id], groups: [] })
|
|
|
+ .catch(error => {
|
|
|
+ logger.error(`could not install ${app.id}`, { error })
|
|
|
+ app.installationError = true
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ logger.info(`installed ${app.id}`)
|
|
|
+ app.loading = false
|
|
|
+ })
|
|
|
+ }))
|
|
|
+ logger.debug(`installing ${installing.length} recommended apps`)
|
|
|
+ Promise.all(installing)
|
|
|
+ .then(() => {
|
|
|
+ logger.info('all recommended apps installed, redirecting …')
|
|
|
+
|
|
|
+ window.location = defaultPageUrl
|
|
|
+ })
|
|
|
+ .catch(error => logger.error('could not install recommended apps', { error }))
|
|
|
+ },
|
|
|
+ customIcon(appId) {
|
|
|
+ if (!(appId in recommended)) {
|
|
|
+ logger.warn(`no app icon for recommended app ${appId}`)
|
|
|
+ return imagePath('core', 'places/default-app-icon.svg')
|
|
|
+ }
|
|
|
+ return recommended[appId].icon
|
|
|
+ },
|
|
|
+ customDescription(appId) {
|
|
|
+ if (!(appId in recommended)) {
|
|
|
+ logger.warn(`no app description for recommended app ${appId}`)
|
|
|
+ return ''
|
|
|
+ }
|
|
|
+ return recommended[appId].description
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+p.loading, p.loading-error {
|
|
|
+ height: 100px;
|
|
|
+}
|
|
|
+.app {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: row;
|
|
|
+
|
|
|
+ img {
|
|
|
+ height: 64px;
|
|
|
+ width: 64px;
|
|
|
+ }
|
|
|
+
|
|
|
+ img, .info {
|
|
|
+ padding: 12px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .info {
|
|
|
+ h3 {
|
|
|
+ text-align: left;
|
|
|
+ }
|
|
|
+
|
|
|
+ h3 > span.icon {
|
|
|
+ display: inline-block;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|