Skip to content

Commit d0810b4

Browse files
committed
fixup
1 parent 65758ec commit d0810b4

33 files changed

+872
-519
lines changed

apps/appstore/src/AppstoreApp.vue

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
import { t } from '@nextcloud/l10n'
8+
import { computed } from 'vue'
9+
import { useRoute } from 'vue-router'
10+
import NcAppContent from '@nextcloud/vue/components/NcAppContent'
11+
import NcContent from '@nextcloud/vue/components/NcContent'
12+
import AppstoreNavigation from './views/AppstoreNavigation.vue'
13+
import AppstoreSidebar from './views/AppstoreSidebar.vue'
14+
import { APPSTORE_CATEGORY_NAMES } from './constants.ts'
15+
16+
const route = useRoute()
17+
const heading = computed(() => route.params.category
18+
&& (APPSTORE_CATEGORY_NAMES[route.params.category as string]
19+
?? route.params.category))
20+
const pageTitle = computed(() => `${heading.value} - ${t('appstore', 'App store')}`)
21+
22+
const showSidebar = computed(() => !!route.params.id)
23+
</script>
24+
25+
<template>
26+
<NcContent app-name="appstore">
27+
<AppstoreNavigation />
28+
<NcAppContent
29+
:page-heading="t('appstore', 'App store')"
30+
:page-title>
31+
<h2 v-if="heading" :class="$style.appstoreApp__heading">
32+
{{ heading }}
33+
</h2>
34+
<router-view />
35+
</NcAppContent>
36+
<AppstoreSidebar v-if="showSidebar" />
37+
</NcContent>
38+
</template>
39+
40+
<style module>
41+
.appstoreApp__heading {
42+
margin-block-start: var(--app-navigation-padding);
43+
margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);
44+
min-height: var(--default-clickable-area);
45+
line-height: var(--default-clickable-area);
46+
vertical-align: center;
47+
}
48+
</style>
Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,16 @@ export interface IAppstoreAppRelease {
2727
}
2828
}
2929

30-
export interface IAppstoreApp {
30+
export interface IAppstoreAppData extends Record<string, unknown> {
31+
ratingOverall: number
32+
ratingNumOverall: number
33+
ratingRecent: number
34+
ratingNumRecent: number
35+
36+
releases: IAppstoreAppRelease[]
37+
}
38+
39+
export interface IAppstoreAppResponse {
3140
id: string
3241
name: string
3342
summary: string
@@ -38,10 +47,12 @@ export interface IAppstoreApp {
3847
version: string
3948
category: string | string[]
4049

41-
preview?: string
4250
screenshot?: string
4351

44-
app_api: boolean
52+
score: number
53+
ratingNumThresholdReached: boolean
54+
55+
app_api: false
4556
active: boolean
4657
internal: boolean
4758
removable: boolean
@@ -52,10 +63,14 @@ export interface IAppstoreApp {
5263
needsDownload: boolean
5364
update?: string
5465

55-
appstoreData: Record<string, never>
66+
appstoreData?: IAppstoreAppData
5667
releases?: IAppstoreAppRelease[]
5768
}
5869

70+
export interface IAppstoreApp extends IAppstoreAppResponse {
71+
loading?: boolean
72+
}
73+
5974
export interface IComputeDevice {
6075
id: string
6176
label: string
@@ -81,10 +96,10 @@ export interface IDeployDaemon {
8196
export interface IExAppStatus {
8297
action: string
8398
deploy: number
84-
deploy_start_time: number
85-
error: string
99+
deploy_start_time?: number
100+
error?: string
86101
init: number
87-
init_start_time: number
102+
init_start_time?: number
88103
type: string
89104
}
90105

@@ -111,8 +126,9 @@ export interface IAppstoreExAppRelease extends IAppstoreAppRelease {
111126
}
112127

113128
export interface IAppstoreExApp extends IAppstoreApp {
129+
app_api: true
114130
daemon: IDeployDaemon | null | undefined
115131
status: IExAppStatus | Record<string, never>
116-
error: string
132+
error?: string
117133
releases: IAppstoreExAppRelease[]
118134
}

apps/appstore/src/apps.js

Lines changed: 0 additions & 10 deletions
This file was deleted.
File renamed without changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<script setup lang="ts">
2+
import type { IAppstoreApp, IAppstoreExApp } from '../apps.d.ts'
3+
4+
import { mdiCogOutline } from '@mdi/js'
5+
import { NcLoadingIcon } from '@nextcloud/vue'
6+
import { ref, watchEffect } from 'vue'
7+
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
8+
9+
const props = defineProps<{
10+
app: IAppstoreApp | IAppstoreExApp
11+
}>()
12+
13+
const isError = ref(false)
14+
const isLoading = ref(true)
15+
watchEffect(() => {
16+
if (props.app.screenshot) {
17+
isError.value = false
18+
isLoading.value = true
19+
const image = new Image()
20+
image.onload = () => {
21+
isLoading.value = false
22+
}
23+
image.onerror = () => {
24+
isError.value = true
25+
isLoading.value = false
26+
}
27+
image.src = props.app.screenshot
28+
} else {
29+
isLoading.value = false
30+
isError.value = false
31+
}
32+
})
33+
</script>
34+
35+
<template>
36+
<div :class="$style.appImage">
37+
<NcIconSvgWrapper
38+
v-if="isError || !props.app.screenshot"
39+
:size="80"
40+
:path="mdiCogOutline" />
41+
42+
<NcLoadingIcon v-else-if="isLoading" :size="80" />
43+
44+
<img :class="$style.appImage__image" :src="props.app.screenshot" alt="">
45+
</div>
46+
</template>
47+
48+
<style module>
49+
.appImage {
50+
display: flex;
51+
justify-content: center;
52+
width: 100%;
53+
height: 100%;
54+
}
55+
56+
.appImage__image {
57+
object-fit: cover;
58+
height: 100%;
59+
width: 100%;
60+
}
61+
</style>
File renamed without changes.
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
6+
<script setup lang="ts">
7+
/**
8+
* This component either shows a native link to the installed app or external size
9+
* or a router link to the appstore page of the app if not installed
10+
*/
11+
12+
import type { RouterLinkProps } from 'vue-router'
13+
import type { INavigationEntry } from '../../../../core/src/types/navigation.d.ts'
14+
15+
import { loadState } from '@nextcloud/initial-state'
16+
import { generateUrl } from '@nextcloud/router'
17+
import { ref, watchEffect } from 'vue'
18+
import { RouterLink, useRoute } from 'vue-router'
19+
20+
const props = defineProps<{
21+
href: string
22+
}>()
23+
24+
const route = useRoute()
25+
const knownRoutes = Object.fromEntries(loadState<INavigationEntry[]>('core', 'apps').map((app) => [app.app ?? app.id, app.href]))
26+
27+
const routerProps = ref<RouterLinkProps>()
28+
const linkProps = ref<Record<string, string>>()
29+
30+
watchEffect(() => {
31+
const match = props.href.match(/^app:(\/\/)?([^/]+)(\/.+)?$/)
32+
routerProps.value = undefined
33+
linkProps.value = undefined
34+
35+
// not an app url
36+
if (match === null) {
37+
linkProps.value = {
38+
href: props.href,
39+
target: '_blank',
40+
rel: 'noreferrer noopener',
41+
}
42+
return
43+
}
44+
45+
const appId = match[2]!
46+
// Check if specific route was requested
47+
if (match[3]) {
48+
// we do no know anything about app internal path so we only allow generic app paths
49+
linkProps.value = {
50+
href: generateUrl(`/apps/${appId}${match[3]}`),
51+
}
52+
return
53+
}
54+
55+
// If we know any route for that app we open it
56+
if (appId in knownRoutes) {
57+
linkProps.value = {
58+
href: knownRoutes[appId]!,
59+
}
60+
return
61+
}
62+
63+
// Fallback to show the app store entry
64+
routerProps.value = {
65+
to: {
66+
name: 'apps-details',
67+
params: {
68+
category: route.params?.category ?? 'discover',
69+
id: appId,
70+
},
71+
},
72+
}
73+
})
74+
</script>
75+
76+
<template>
77+
<a v-if="linkProps" v-bind="linkProps">
78+
<slot />
79+
</a>
80+
<RouterLink v-else-if="routerProps" v-bind="routerProps">
81+
<slot />
82+
</RouterLink>
83+
</template>
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<li
7+
:class="[$style.appListItem, {
8+
[$style.appListItem_selected]: isSelected,
9+
}]">
10+
<div class="app-image app-image-icon">
11+
<div v-if="!app?.app_api && !props.app.preview" class="icon-settings-dark" />
12+
<NcIconSvgWrapper
13+
v-else-if="app.app_api && !props.app.preview"
14+
:path="mdiCogOutline"
15+
:size="24"
16+
style="min-width: auto; min-height: auto; height: 100%;" />
17+
18+
<svg
19+
v-else-if="app.preview && !app.app_api"
20+
width="32"
21+
height="32"
22+
viewBox="0 0 32 32">
23+
<image
24+
x="0"
25+
y="0"
26+
width="32"
27+
height="32"
28+
preserveAspectRatio="xMinYMin meet"
29+
:xlink:href="app.preview"
30+
class="app-icon" />
31+
</svg>
32+
</div>
33+
<div class="app-name">
34+
<router-link
35+
class="app-name--link"
36+
:to="{
37+
name: 'apps-details',
38+
params: {
39+
category: category,
40+
id: app.id,
41+
},
42+
}"
43+
:aria-label="t('settings', 'Show details for {appName} app', { appName: app.name })">
44+
{{ app.name }}
45+
</router-link>
46+
</div>
47+
<AppListVersion :app />
48+
<div class="app-level">
49+
<AppLevelBadge :level="app.level" />
50+
</div>
51+
</li>
52+
</template>
53+
54+
<script setup lang="ts">
55+
import type { IAppstoreApp } from '../../apps.d.ts'
56+
57+
import { mdiCogOutline } from '@mdi/js'
58+
import { t } from '@nextcloud/l10n'
59+
import { ref, watchEffect } from 'vue'
60+
import { useRoute } from 'vue-router'
61+
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
62+
import AppLevelBadge from './AppLevelBadge.vue'
63+
import AppListVersion from './AppListVersion.vue'
64+
65+
const props = defineProps<{
66+
app: IAppstoreApp
67+
category: string
68+
}>()
69+
70+
const route = useRoute()
71+
const isSelected = ref(false)
72+
watchEffect(() => {
73+
isSelected.value = props.app.id === route.params.id
74+
})
75+
76+
const screenshotLoaded = ref(false)
77+
watchEffect(() => {
78+
if (props.app.screenshot) {
79+
const image = new Image()
80+
image.onload = () => {
81+
screenshotLoaded.value = true
82+
}
83+
image.src = props.app.screenshot
84+
}
85+
})
86+
</script>
87+
88+
<style module>
89+
.appListItem {
90+
--app-item-padding: calc(var(--default-grid-baseline) * 2);
91+
--app-item-height: calc(var(--default-clickable-area) + var(--app-item-padding) * 2);
92+
93+
> * {
94+
vertical-align: middle;
95+
border-bottom: 1px solid var(--color-border);
96+
padding: var(--app-item-padding);
97+
height: var(--app-item-height);
98+
}
99+
}
100+
101+
.appListItem:hover {
102+
background-color: var(--color-background-dark);
103+
}
104+
105+
.appListItem_selected {
106+
background-color: var(--color-background-dark);
107+
}
108+
</style>

0 commit comments

Comments
 (0)