Skip to content

Commit 400c3b3

Browse files
Merge pull request #44328 from nextcloud/feat/app-discover-showcase-type
feat(settings): Implement `showcase` type for App Discover section
2 parents 83746f7 + d58f3e3 commit 400c3b3

20 files changed

Lines changed: 320 additions & 52 deletions

apps/settings/src/components/AppList/AppItem.vue

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
-->
2222

2323
<template>
24-
<component :is="listView ? `tr` : `li`"
24+
<component :is="listView ? 'tr' : (inline ? 'article' : 'li')"
2525
class="app-item"
2626
:class="{
2727
'app-item--list-view': listView,
@@ -82,7 +82,10 @@
8282
<AppLevelBadge :level="app.level" />
8383
<AppScore v-if="hasRating && !listView" :score="app.score" />
8484
</component>
85-
<component :is="dataItemTag" :headers="getDataItemHeaders(`app-table-col-actions`)" class="app-actions">
85+
<component :is="dataItemTag"
86+
v-if="!inline"
87+
:headers="getDataItemHeaders(`app-table-col-actions`)"
88+
class="app-actions">
8689
<div v-if="app.error" class="warning">
8790
{{ app.error }}
8891
</div>
@@ -145,7 +148,10 @@ export default {
145148
type: Object,
146149
required: true,
147150
},
148-
category: {},
151+
category: {
152+
type: String,
153+
required: true,
154+
},
149155
listView: {
150156
type: Boolean,
151157
default: true,
@@ -158,6 +164,10 @@ export default {
158164
type: String,
159165
default: null,
160166
},
167+
inline: {
168+
type: Boolean,
169+
default: false,
170+
},
161171
},
162172
data() {
163173
return {

apps/settings/src/components/AppStoreDiscover/AppStoreDiscoverSection.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { parseApiResponse, filterElements } from '../../utils/appDiscoverParser.
4242
4343
const PostType = defineAsyncComponent(() => import('./PostType.vue'))
4444
const CarouselType = defineAsyncComponent(() => import('./CarouselType.vue'))
45+
const ShowcaseType = defineAsyncComponent(() => import('./ShowcaseType.vue'))
4546
4647
const hasError = ref(false)
4748
const elements = ref<IAppDiscoverElements[]>([])
@@ -89,6 +90,8 @@ const getComponent = (type) => {
8990
return PostType
9091
} else if (type === 'carousel') {
9192
return CarouselType
93+
} else if (type === 'showcase') {
94+
return ShowcaseType
9295
}
9396
return defineComponent({
9497
mounted: () => logger.error('Unknown component requested ', type),
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<!--
2+
- @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
3+
-
4+
- @author Ferdinand Thiessen <opensource@fthiessen.de>
5+
-
6+
- @license AGPL-3.0-or-later
7+
-
8+
- This program is free software: you can redistribute it and/or modify
9+
- it under the terms of the GNU Affero General Public License as
10+
- published by the Free Software Foundation, either version 3 of the
11+
- License, or (at your option) any later version.
12+
-
13+
- This program is distributed in the hope that it will be useful,
14+
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
- GNU Affero General Public License for more details.
17+
-
18+
- You should have received a copy of the GNU Affero General Public License
19+
- along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
-
21+
-->
22+
<template>
23+
<AppItem v-if="app"
24+
:app="app"
25+
category="discover"
26+
class="app-discover-app"
27+
inline
28+
:list-view="false" />
29+
<a v-else
30+
class="app-discover-app app-discover-app__skeleton"
31+
:href="appStoreLink"
32+
target="_blank"
33+
:title="modelValue.appId"
34+
rel="noopener noreferrer">
35+
<!-- This is a fallback skeleton -->
36+
<span class="skeleton-element" />
37+
<span class="skeleton-element" />
38+
<span class="skeleton-element" />
39+
<span class="skeleton-element" />
40+
<span class="skeleton-element" />
41+
</a>
42+
</template>
43+
44+
<script setup lang="ts">
45+
import type { IAppDiscoverApp } from '../../constants/AppDiscoverTypes'
46+
47+
import { computed } from 'vue'
48+
import { useAppsStore } from '../../store/apps-store.ts'
49+
50+
import AppItem from '../AppList/AppItem.vue'
51+
52+
const props = defineProps<{
53+
modelValue: IAppDiscoverApp
54+
}>()
55+
56+
const store = useAppsStore()
57+
const app = computed(() => store.getAppById(props.modelValue.appId))
58+
59+
const appStoreLink = computed(() => props.modelValue.appId ? `https://apps.nextcloud.com/apps/${props.modelValue.appId}` : '#')
60+
</script>
61+
62+
<style scoped lang="scss">
63+
.app-discover-app {
64+
width: 100% !important; // full with of the showcase item
65+
66+
&:hover {
67+
background: var(--color-background-hover);
68+
border-radius: var(--border-radius-rounded);
69+
}
70+
71+
&__skeleton {
72+
display: flex;
73+
flex-direction: column;
74+
gap: 8px;
75+
76+
padding: 30px; // Same as AppItem
77+
78+
> :first-child {
79+
height: 50%;
80+
min-height: 130px;
81+
}
82+
83+
> :nth-child(2) {
84+
width: 50px;
85+
}
86+
87+
> :nth-child(5) {
88+
height: 20px;
89+
width: 100px;
90+
}
91+
92+
> :not(:first-child) {
93+
border-radius: 4px;
94+
}
95+
}
96+
}
97+
98+
.skeleton-element {
99+
min-height: var(--default-font-size, 15px);
100+
101+
background: linear-gradient(90deg, var(--color-background-dark), var(--color-background-darker), var(--color-background-dark));
102+
background-size: 400% 400%;
103+
animation: gradient 6s ease infinite;
104+
}
105+
106+
@keyframes gradient {
107+
0% {
108+
background-position: 0% 50%;
109+
}
110+
50% {
111+
background-position: 100% 50%;
112+
}
113+
100% {
114+
background-position: 0% 50%;
115+
}
116+
}
117+
</style>

apps/settings/src/components/AppStoreDiscover/PostType.vue

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121
-->
2222
<template>
2323
<article :id="domId"
24+
ref="container"
2425
class="app-discover-post"
25-
:class="{ 'app-discover-post--reverse': media && media.alignment === 'start' }">
26+
:class="{
27+
'app-discover-post--reverse': media && media.alignment === 'start',
28+
'app-discover-post--small': isSmallWidth
29+
}">
2630
<component :is="link ? 'AppLink' : 'div'"
2731
v-if="headline || text"
2832
:href="link"
@@ -73,7 +77,7 @@ import type { PropType } from 'vue'
7377
7478
import { mdiPlayCircleOutline } from '@mdi/js'
7579
import { generateUrl } from '@nextcloud/router'
76-
import { useElementVisibility } from '@vueuse/core'
80+
import { useElementSize, useElementVisibility } from '@vueuse/core'
7781
import { computed, defineComponent, ref, watchEffect } from 'vue'
7882
import { commonAppDiscoverProps } from './common'
7983
import { useLocalizedValue } from '../../composables/useGetLocalizedValue'
@@ -138,6 +142,14 @@ export default defineComponent({
138142
const hasPlaybackEnded = ref(false)
139143
const showPlayVideo = computed(() => localizedMedia.value?.link && hasPlaybackEnded.value)
140144
145+
/**
146+
* The content is sized / styles are applied based on the container width
147+
* To make it responsive even for inline usage and when opening / closing the sidebar / navigation
148+
*/
149+
const container = ref<HTMLElement>()
150+
const { width: containerWidth } = useElementSize(container)
151+
const isSmallWidth = computed(() => containerWidth.value < 600)
152+
141153
/**
142154
* Generate URL for cached media to prevent user can be tracked
143155
* @param url The URL to resolve
@@ -171,6 +183,8 @@ export default defineComponent({
171183
return {
172184
mdiPlayCircleOutline,
173185
186+
container,
187+
174188
translatedText,
175189
translatedHeadline,
176190
mediaElement,
@@ -182,6 +196,7 @@ export default defineComponent({
182196
showPlayVideo,
183197
184198
isFullWidth,
199+
isSmallWidth,
185200
isImage,
186201
187202
generatePrivacyUrl,
@@ -199,6 +214,8 @@ export default defineComponent({
199214
200215
display: flex;
201216
flex-direction: row;
217+
justify-content: start;
218+
202219
&--reverse {
203220
flex-direction: row-reverse;
204221
}
@@ -264,16 +281,17 @@ export default defineComponent({
264281
}
265282
}
266283
267-
// Ensure section works on mobile devices
268-
@media only screen and (max-width: 699px) {
269-
.app-discover-post {
284+
.app-discover-post--small {
285+
&.app-discover-post {
270286
flex-direction: column;
271287
max-height: 500px;
272288
273289
&--reverse {
274290
flex-direction: column-reverse;
275291
}
292+
}
276293
294+
.app-discover-post {
277295
&__text {
278296
flex: 1 1 50%;
279297
}

0 commit comments

Comments
 (0)