aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components/AppList/AppItem.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components/AppList/AppItem.vue')
-rw-r--r--apps/settings/src/components/AppList/AppItem.vue406
1 files changed, 324 insertions, 82 deletions
diff --git a/apps/settings/src/components/AppList/AppItem.vue b/apps/settings/src/components/AppList/AppItem.vue
index cede668d51f..95a98a93cde 100644
--- a/apps/settings/src/components/AppList/AppItem.vue
+++ b/apps/settings/src/components/AppList/AppItem.vue
@@ -1,31 +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>
- <div class="section" :class="{ selected: isSelected }" @click="showAppDetails">
- <div class="app-image app-image-icon" @click="showAppDetails">
- <div v-if="(listView && !app.preview) || (!listView && !screenshotLoaded)" class="icon-settings-dark" />
+ <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="!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">
@@ -38,41 +33,53 @@
class="app-icon" />
</svg>
- <img v-if="!listView && app.screenshot && screenshotLoaded" :src="app.screenshot" width="100%">
- </div>
- <div class="app-name" @click="showAppDetails">
- {{ app.name }}
- </div>
- <div v-if="!listView" class="app-summary">
+ <img v-if="!listView && app.screenshot && screenshotLoaded" :src="app.screenshot" alt="">
+ </component>
+ <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
+ },
+ }"
+ :aria-label="t('settings', 'Show details for {appName} app', { appName:app.name })">
+ {{ app.name }}
+ </router-link>
+ </component>
+ <component :is="dataItemTag"
+ v-if="!listView"
+ class="app-summary"
+ :headers="getDataItemHeaders(`app-version`)">
{{ app.summary }}
- </div>
- <div v-if="listView" class="app-version">
+ </component>
+ <component :is="dataItemTag"
+ v-if="listView"
+ class="app-version"
+ :headers="getDataItemHeaders(`app-table-col-version`)">
<span v-if="app.version">{{ app.version }}</span>
<span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
- </div>
-
- <div 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>
- <AppScore v-if="hasRating && !listView" :score="app.score" />
- </div>
+ </component>
- <div class="actions">
+ <component :is="dataItemTag" :headers="getDataItemHeaders(`app-table-col-level`)" class="app-level">
+ <AppLevelBadge :level="app.level" />
+ <AppScore v-if="hasRating && !listView" :score="app.score" />
+ </component>
+ <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>
@@ -84,62 +91,114 @@
{{ 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>
- </div>
- </div>
+
+ <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,
},
+ useBundleView: {
+ type: Boolean,
+ default: false,
+ },
+ headers: {
+ 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: {
hasRating() {
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
},
+ 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) {
@@ -150,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
@@ -160,34 +219,217 @@ export default {
},
methods: {
- async showAppDetails(event) {
- if (event.currentTarget.tagName === 'INPUT' || event.currentTarget.tagName === 'A') {
+ prefix(prefix, content) {
+ return prefix + '_' + content
+ },
+
+ 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
}
- try {
- await this.$router.push({
- name: 'apps-details',
- params: { category: this.category, id: this.app.id },
- })
- } catch (e) {
- // we already view this app
+ 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)
}
},
- prefix(prefix, content) {
- return prefix + '_' + content
- },
},
}
</script>
-<style scoped>
- .app-icon {
- filter: var(--background-invert-if-bright);
+<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);
}
- .actions {
- display: flex !important;
- gap: 8px;
- flex-wrap: wrap;
- justify-content: end;
+
+ &--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 {
+ position: relative;
+ height: 150px;
+ opacity: 1;
+ overflow: hidden;
+
+ img {
+ width: 100%;
}
+}
+
+.app-version {
+ color: var(--color-text-maxcontrast);
+}
</style>