Skip to content

Commit 62187b7

Browse files
committed
refactor(appstore): migrate appstore views to Vue 3 and Typescript
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent 24f7420 commit 62187b7

27 files changed

Lines changed: 925 additions & 1388 deletions

apps/appstore/src/AppstoreApp.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import NcContent from '@nextcloud/vue/components/NcContent'
1212
import AppstoreNavigation from './views/AppstoreNavigation.vue'
1313
import AppstoreSidebar from './views/AppstoreSidebar.vue'
1414
import { APPSTORE_CATEGORY_NAMES } from './constants.ts'
15+
import { useAppsStore } from './store/apps.ts'
1516
1617
const route = useRoute()
18+
const store = useAppsStore()
1719
1820
const currentCategory = computed(() => {
1921
if (route.params.category) {
@@ -24,7 +26,13 @@ const currentCategory = computed(() => {
2426
}
2527
return 'discover'
2628
})
27-
const heading = computed(() => APPSTORE_CATEGORY_NAMES[currentCategory.value] ?? currentCategory.value)
29+
30+
const heading = computed(() => {
31+
if (currentCategory.value in APPSTORE_CATEGORY_NAMES) {
32+
return APPSTORE_CATEGORY_NAMES[currentCategory.value]
33+
}
34+
return store.getCategoryById(currentCategory.value)?.displayName ?? currentCategory.value
35+
})
2836
const pageTitle = computed(() => `${heading.value} - ${t('appstore', 'App store')}`)
2937
3038
const showSidebar = computed(() => !!route.params.id)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { IAppstoreApp, IAppstoreExApp } from '../../apps.d.ts'
8+
9+
import { computed } from 'vue'
10+
import AppGridItem from './AppGridItem.vue'
11+
import { useUserSettingsStore } from '../../store/userSettings.ts'
12+
13+
defineProps<{
14+
apps: (IAppstoreApp | IAppstoreExApp)[]
15+
}>()
16+
17+
const userSettings = useUserSettingsStore()
18+
const gridSize = computed(() => userSettings.gridSizePx)
19+
</script>
20+
21+
<template>
22+
<ul :class="$style.appGrid">
23+
<AppGridItem
24+
v-for="app in apps"
25+
:key="app.id"
26+
:app />
27+
</ul>
28+
</template>
29+
30+
<style module>
31+
.appGrid {
32+
width: 100%;
33+
display: grid;
34+
gap: calc(4 * var(--default-grid-baseline));
35+
grid-template-columns: repeat(auto-fit, minmax(v-bind(gridSize), 1fr));
36+
padding-inline-start: var(--app-navigation-padding);
37+
}
38+
</style>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import type { IAppstoreApp, IAppstoreExApp } from '../../apps.d.ts'
8+
9+
import { computed } from 'vue'
10+
import { useRoute } from 'vue-router'
11+
import AppImage from '../AppImage.vue'
12+
import BadgeAppDaemon from '../BadgeAppDaemon.vue'
13+
import BadgeAppLevel from '../BadgeAppLevel.vue'
14+
import BadgeAppScore from '../BadgeAppScore.vue'
15+
import { useUserSettingsStore } from '../../store/userSettings.ts'
16+
17+
const { app } = defineProps<{
18+
app: IAppstoreApp | IAppstoreExApp
19+
}>()
20+
21+
const userSettingsStore = useUserSettingsStore()
22+
const route = useRoute()
23+
const routeToDetails = computed(() => ({
24+
...route,
25+
params: {
26+
...route.params,
27+
id: app.id,
28+
},
29+
query: userSettingsStore.getQuery(),
30+
}))
31+
</script>
32+
33+
<template>
34+
<li :class="$style.appGridItem">
35+
<RouterLink :to="routeToDetails">
36+
<AppImage :app :class="$style.appGridItem__image" />
37+
<div :class="$style.appGridItem__content">
38+
<h3 :class="$style.appGridItem__name">
39+
{{ app.name }}
40+
</h3>
41+
<p>{{ app.summary }}</p>
42+
</div>
43+
</RouterLink>
44+
<div :class="$style.appGridItem__badges">
45+
<BadgeAppScore :app />
46+
<BadgeAppLevel :level="app.level" />
47+
<BadgeAppDaemon v-if="app.app_api && app.daemon" :daemon="app.daemon" />
48+
</div>
49+
</li>
50+
</template>
51+
52+
<style module>
53+
.appGridItem {
54+
background-color: var(--color-primary-element-light);
55+
color: var(--color-primary-element-light-text);
56+
border-radius: var(--border-radius-element);
57+
padding-block-end: var(--border-radius-element);;
58+
display: flex;
59+
flex-direction: column;
60+
justify-content: space-between;
61+
gap: var(--default-grid-baseline);
62+
63+
&:hover {
64+
background-color: var(--color-primary-element-light-hover);
65+
}
66+
}
67+
68+
.appGridItem__content {
69+
padding-inline: var(--border-radius-element);
70+
}
71+
72+
.appGridItem__image {
73+
aspect-ratio: 16 / 9;
74+
height: min-content;
75+
border-start-start-radius: var(--border-radius-element);
76+
border-start-end-radius: var(--border-radius-element);
77+
overflow: hidden;
78+
}
79+
80+
.appGridItem__name {
81+
font-size: 1.2em;
82+
font-weight: var(--font-weight-heading, bold);
83+
margin-block: var(--default-grid-baseline) calc(2 * var(--default-grid-baseline));
84+
}
85+
86+
.appGridItem__badges {
87+
display: flex;
88+
flex-direction: row;
89+
gap: var(--default-grid-baseline);
90+
padding-inline: var(--border-radius-element);
91+
width: 100%;
92+
}
93+
</style>

apps/appstore/src/components/AppImage.vue

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
88
99
import { mdiCogOutline } from '@mdi/js'
1010
import { NcLoadingIcon } from '@nextcloud/vue'
11+
import PQueue from 'p-queue'
1112
import { ref, watchEffect } from 'vue'
1213
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
1314
@@ -21,22 +22,28 @@ watchEffect(() => {
2122
if (props.app.screenshot) {
2223
isError.value = false
2324
isLoading.value = true
24-
const image = new Image()
25-
image.onload = () => {
26-
isLoading.value = false
27-
}
28-
image.onerror = () => {
29-
isError.value = true
30-
isLoading.value = false
31-
}
32-
image.src = props.app.screenshot
25+
queue.add(() => {
26+
const image = new Image()
27+
image.onload = () => {
28+
isLoading.value = false
29+
}
30+
image.onerror = () => {
31+
isError.value = true
32+
isLoading.value = false
33+
}
34+
image.src = props.app.screenshot!
35+
})
3336
} else {
3437
isLoading.value = false
3538
isError.value = false
3639
}
3740
})
3841
</script>
3942

43+
<script lang="ts">
44+
const queue = new PQueue({ concurrency: 4 })
45+
</script>
46+
4047
<template>
4148
<div :class="$style.appImage">
4249
<NcIconSvgWrapper
@@ -46,7 +53,11 @@ watchEffect(() => {
4653

4754
<NcLoadingIcon v-else-if="isLoading" :size="80" />
4855

49-
<img :class="$style.appImage__image" :src="props.app.screenshot" alt="">
56+
<img
57+
v-else
58+
:class="$style.appImage__image"
59+
:src="props.app.screenshot"
60+
alt="">
5061
</div>
5162
</template>
5263

apps/appstore/src/components/AppLink.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ watchEffect(() => {
6363
// Fallback to show the app store entry
6464
routerProps.value = {
6565
to: {
66-
name: 'apps-details',
66+
name: 'apps-discover',
6767
params: {
6868
category: route.params?.category ?? 'discover',
6969
id: appId,

0 commit comments

Comments
 (0)