diff options
Diffstat (limited to 'apps/settings/src/components/AppList/AppItem.vue')
-rw-r--r-- | apps/settings/src/components/AppList/AppItem.vue | 336 |
1 files changed, 277 insertions, 59 deletions
diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue index 0838f2c8822..95a98a93cde 100644 --- a/apps/settings/src/components/AppList/AppItem.vue +++ b/apps/settings/src/components/AppList/AppItem.vue @@ -1,35 +1,26 @@ <!-- - - @copyright Copyright (c) 2018 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/>. - - - --> - + - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> <template> - <component :is="listView ? `tr` : `li`" - class="section" - :class="{ selected: isSelected }"> + <component :is="listView ? 'tr' : (inline ? 'article' : 'li')" + class="app-item" + :class="{ + 'app-item--list-view': listView, + 'app-item--store-view': !listView, + 'app-item--selected': isSelected, + 'app-item--with-sidebar': withSidebar, + }"> <component :is="dataItemTag" class="app-image app-image-icon" :headers="getDataItemHeaders(`app-table-col-icon`)"> - <div v-if="(listView && !app.preview) || (!listView && !screenshotLoaded)" class="icon-settings-dark" /> + <div v-if="!app?.app_api && shouldDisplayDefaultIcon" class="icon-settings-dark" /> + <NcIconSvgWrapper v-else-if="app.app_api && shouldDisplayDefaultIcon" + :path="mdiCogOutline" + :size="listView ? 24 : 48" + style="min-width: auto; min-height: auto; height: 100%;" /> - <svg v-else-if="listView && app.preview" + <svg v-else-if="listView && app.preview && !app.app_api" width="32" height="32" viewBox="0 0 32 32"> @@ -47,7 +38,14 @@ <component :is="dataItemTag" class="app-name" :headers="getDataItemHeaders(`app-table-col-name`)"> - <router-link class="app-name--link" :to="{ name: 'apps-details', params: { category: category, id: app.id }}" + <router-link class="app-name--link" + :to="{ + name: 'apps-details', + params: { + category: category, + id: app.id + }, + }" :aria-label="t('settings', 'Show details for {appName} app', { appName:app.name })"> {{ app.name }} </router-link> @@ -67,26 +65,21 @@ </component> <component :is="dataItemTag" :headers="getDataItemHeaders(`app-table-col-level`)" class="app-level"> - <span v-if="app.level === 300" - :title="t('settings', 'This app is supported via your current Nextcloud subscription.')" - :aria-label="t('settings', 'This app is supported via your current Nextcloud subscription.')" - class="supported icon-checkmark-color"> - {{ t('settings', 'Supported') }}</span> - <span v-if="app.level === 200" - :title="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')" - :aria-label="t('settings', 'Featured apps are developed by and within the community. They offer central functionality and are ready for production use.')" - class="official icon-checkmark"> - {{ t('settings', 'Featured') }}</span> + <AppLevelBadge :level="app.level" /> <AppScore v-if="hasRating && !listView" :score="app.score" /> </component> - <component :is="dataItemTag" :headers="getDataItemHeaders(`app-table-col-actions`)" class="actions"> + <component :is="dataItemTag" + v-if="!inline" + :headers="getDataItemHeaders(`app-table-col-actions`)" + class="app-actions"> <div v-if="app.error" class="warning"> {{ app.error }} </div> - <div v-if="isLoading" class="icon icon-loading-small" /> + <div v-if="isLoading || isInitializing" class="icon icon-loading-small" /> <NcButton v-if="app.update" type="primary" - :disabled="installing || isLoading" + :disabled="installing || isLoading || !defaultDeployDaemonAccessible || isManualInstall" + :title="updateButtonText" @click.stop="update(app.id)"> {{ t('settings', 'Update to {update}', {update:app.update}) }} </NcButton> @@ -98,46 +91,66 @@ {{ t('settings', 'Remove') }} </NcButton> <NcButton v-if="app.active" - :disabled="installing || isLoading" + :disabled="installing || isLoading || isInitializing || isDeploying" @click.stop="disable(app.id)"> - {{ t('settings','Disable') }} + {{ disableButtonText }} </NcButton> <NcButton v-if="!app.active && (app.canInstall || app.isCompatible)" :title="enableButtonTooltip" :aria-label="enableButtonTooltip" type="primary" - :disabled="!app.canInstall || installing || isLoading" - @click.stop="enable(app.id)"> + :disabled="!app.canInstall || installing || isLoading || !defaultDeployDaemonAccessible || isInitializing || isDeploying" + @click.stop="enableButtonAction"> {{ enableButtonText }} </NcButton> <NcButton v-else-if="!app.active" :title="forceEnableButtonTooltip" :aria-label="forceEnableButtonTooltip" type="secondary" - :disabled="installing || isLoading" + :disabled="installing || isLoading || !defaultDeployDaemonAccessible" @click.stop="forceEnable(app.id)"> {{ forceEnableButtonText }} </NcButton> + + <DaemonSelectionDialog v-if="app?.app_api && showSelectDaemonModal" + :show.sync="showSelectDaemonModal" + :app="app" /> </component> </component> </template> <script> +import { useAppsStore } from '../../store/apps-store.js' + import AppScore from './AppScore.vue' +import AppLevelBadge from './AppLevelBadge.vue' import AppManagement from '../../mixins/AppManagement.js' import SvgFilterMixin from '../SvgFilterMixin.vue' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper' +import { mdiCogOutline } from '@mdi/js' +import { useAppApiStore } from '../../store/app-api-store.ts' +import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue' export default { name: 'AppItem', components: { + AppLevelBadge, AppScore, NcButton, + NcIconSvgWrapper, + DaemonSelectionDialog, }, mixins: [AppManagement, SvgFilterMixin], props: { - app: {}, - category: {}, + app: { + type: Object, + required: true, + }, + category: { + type: String, + required: true, + }, listView: { type: Boolean, default: true, @@ -150,12 +163,27 @@ export default { type: String, default: null, }, + inline: { + type: Boolean, + default: false, + }, + }, + setup() { + const store = useAppsStore() + const appApiStore = useAppApiStore() + + return { + store, + appApiStore, + mdiCogOutline, + } }, data() { return { isSelected: false, scrolled: false, screenshotLoaded: false, + showSelectDaemonModal: false, } }, computed: { @@ -165,6 +193,12 @@ export default { dataItemTag() { return this.listView ? 'td' : 'div' }, + withSidebar() { + return !!this.$route.params.id + }, + shouldDisplayDefaultIcon() { + return (this.listView && !this.app.preview) || (!this.listView && !this.screenshotLoaded) + }, }, watch: { '$route.params.id'(id) { @@ -175,7 +209,7 @@ export default { this.isSelected = (this.app.id === this.$route.params.id) if (this.app.releases && this.app.screenshot) { const image = new Image() - image.onload = (e) => { + image.onload = () => { this.screenshotLoaded = true } image.src = this.app.screenshot @@ -192,26 +226,210 @@ export default { getDataItemHeaders(columnName) { return this.useBundleView ? [this.headers, columnName].join(' ') : null }, + showSelectionModal() { + this.showSelectDaemonModal = true + }, + async enableButtonAction() { + if (!this.app?.app_api) { + this.enable(this.app.id) + return + } + await this.appApiStore.fetchDockerDaemons() + if (this.appApiStore.dockerDaemons.length === 1 && this.app.needsDownload) { + this.enable(this.app.id, this.appApiStore.dockerDaemons[0]) + } else if (this.app.needsDownload) { + this.showSelectionModal() + } else { + this.enable(this.app.id, this.app.daemon) + } + }, }, } </script> <style scoped lang="scss"> +@use '../../../../../core/css/variables.scss' as variables; +@use 'sass:math'; + +.app-item { + position: relative; + + &:hover { + background-color: var(--color-background-dark); + } + + &--list-view { + --app-item-padding: calc(var(--default-grid-baseline) * 2); + --app-item-height: calc(var(--default-clickable-area) + var(--app-item-padding) * 2); + + &.app-item--selected { + background-color: var(--color-background-dark); + } + + > * { + vertical-align: middle; + border-bottom: 1px solid var(--color-border); + padding: var(--app-item-padding); + height: var(--app-item-height); + } + + .app-image { + width: var(--default-clickable-area); + height: auto; + text-align: end; + } + + .app-image-icon svg, + .app-image-icon .icon-settings-dark { + margin-top: 5px; + width: 20px; + height: 20px; + opacity: .5; + background-size: cover; + display: inline-block; + } + + .app-name { + padding: 0 var(--app-item-padding); + } + + .app-name--link { + height: var(--app-item-height); + display: flex; + align-items: center; + } + + // Note: because of Safari bug, we cannot position link overlay relative to the table row + // So we need to manually position it relative to the table container and cell + // See: https://bugs.webkit.org/show_bug.cgi?id=240961 + .app-name--link::after { + content: ''; + position: absolute; + inset-inline: 0; + height: var(--app-item-height); + } + + .app-actions { + display: flex; + gap: var(--app-item-padding); + flex-wrap: wrap; + justify-content: end; + + .icon-loading-small { + display: inline-block; + top: 4px; + margin-inline-end: 10px; + } + } + + /* hide app version and level on narrower screens */ + @media only screen and (max-width: 900px) { + .app-version, + .app-level { + display: none; + } + } + + /* Hide actions on a small screen. Click on app opens fill-screen sidebar with the buttons */ + @media only screen and (max-width: math.div(variables.$breakpoint-mobile, 2)) { + .app-actions { + display: none; + } + } + } + + &--store-view { + padding: 30px; + + .app-image-icon .icon-settings-dark { + width: 100%; + height: 150px; + background-size: 45px; + opacity: 0.5; + } + + .app-image-icon svg { + position: absolute; + bottom: 43px; + /* position halfway vertically */ + width: 64px; + height: 64px; + opacity: .1; + } + + .app-name { + margin: 5px 0; + } + + .app-name--link::after { + content: ''; + position: absolute; + inset-block: 0; + inset-inline: 0; + } + + .app-actions { + margin: 10px 0; + } + + @media only screen and (min-width: 1601px) { + width: 25%; + + &.app-item--with-sidebar { + width: 33%; + } + } + + @media only screen and (max-width: 1600px) { + width: 25%; + + &.app-item--with-sidebar { + width: 33%; + } + } + + @media only screen and (max-width: 1400px) { + width: 33%; + + &.app-item--with-sidebar { + width: 50%; + } + } + + @media only screen and (max-width: 900px) { + width: 50%; + + &.app-item--with-sidebar { + width: 100%; + } + } + + @media only screen and (max-width: variables.$breakpoint-mobile) { + width: 50%; + } + + @media only screen and (max-width: 480px) { + width: 100%; + } + } +} + .app-icon { filter: var(--background-invert-if-bright); } -.app-image img { - width: 100%; -} +.app-image { + position: relative; + height: 150px; + opacity: 1; + overflow: hidden; -.app-name--link::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; + img { + width: 100%; + } } +.app-version { + color: var(--color-text-maxcontrast); +} </style> |