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.vue336
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>