Skip to content

Commit 36a37f5

Browse files
committed
fixup
1 parent 524614a commit 36a37f5

9 files changed

Lines changed: 466 additions & 107 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 { mdiCogOutline } from '@mdi/js'
10+
import { NcLoadingIcon } from '@nextcloud/vue'
11+
import { ref, watchEffect } from 'vue'
12+
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
13+
14+
const props = defineProps<{
15+
app: IAppstoreApp | IAppstoreExApp
16+
}>()
17+
18+
const isError = ref(false)
19+
const isLoading = ref(true)
20+
watchEffect(() => {
21+
if (props.app.screenshot) {
22+
isError.value = false
23+
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
33+
} else {
34+
isLoading.value = false
35+
isError.value = false
36+
}
37+
})
38+
</script>
39+
40+
<template>
41+
<div :class="$style.appImage">
42+
<NcIconSvgWrapper
43+
v-if="isError || !props.app.screenshot"
44+
:size="80"
45+
:path="mdiCogOutline" />
46+
47+
<NcLoadingIcon v-else-if="isLoading" :size="80" />
48+
49+
<img :class="$style.appImage__image" :src="props.app.screenshot" alt="">
50+
</div>
51+
</template>
52+
53+
<style module>
54+
.appImage {
55+
display: flex;
56+
justify-content: center;
57+
width: 100%;
58+
height: 100%;
59+
}
60+
61+
.appImage__image {
62+
object-fit: cover;
63+
height: 100%;
64+
width: 100%;
65+
}
66+
</style>
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('appstore', '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>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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 } from '../../apps.d.ts'
8+
9+
defineProps<{
10+
app: IAppstoreApp
11+
}>()
12+
</script>
13+
14+
<template>
15+
<div :class="$style.appListVersion">
16+
<span v-if="app.version">{{ app.version }}</span>
17+
<span v-else-if="app.appstoreData?.releases[0]?.version">
18+
{{ app.appstoreData.releases[0].version }}
19+
</span>
20+
</div>
21+
</template>
22+
23+
<style module>
24+
.appListVersion {
25+
color: var(--color-text-maxcontrast);
26+
}
27+
</style>

apps/appstore/src/components/AppList/AppItem.vue renamed to apps/appstore/src/components/AppList/AppTable.vue

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
id: app.id,
5454
},
5555
}"
56-
:aria-label="t('settings', 'Show details for {appName} app', { appName: app.name })">
56+
:aria-label="t('appstore', 'Show details for {appName} app', { appName: app.name })">
5757
{{ app.name }}
5858
</router-link>
5959
</component>
@@ -92,15 +92,15 @@
9292
:disabled="installing || isLoading || !defaultDeployDaemonAccessible || isManualInstall"
9393
:title="updateButtonText"
9494
@click.stop="update(app.id)">
95-
{{ t('settings', 'Update to {update}', { update: app.update }) }}
95+
{{ t('appstore', 'Update to {update}', { update: app.update }) }}
9696
</NcButton>
9797
<NcButton
9898
v-if="app.canUnInstall"
9999
class="uninstall"
100100
variant="tertiary"
101101
:disabled="installing || isLoading"
102102
@click.stop="remove(app.id)">
103-
{{ t('settings', 'Remove') }}
103+
{{ t('appstore', 'Remove') }}
104104
</NcButton>
105105
<NcButton
106106
v-if="app.active"
@@ -129,7 +129,7 @@
129129

130130
<DaemonSelectionDialog
131131
v-if="app?.app_api && showSelectDaemonModal"
132-
:show.sync="showSelectDaemonModal"
132+
v-model:show="showSelectDaemonModal"
133133
:app="app" />
134134
</component>
135135
</component>
@@ -139,13 +139,12 @@
139139
import { mdiCogOutline } from '@mdi/js'
140140
import NcButton from '@nextcloud/vue/components/NcButton'
141141
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
142-
import DaemonSelectionDialog from '../AppAPI/DaemonSelectionDialog.vue'
143-
import SvgFilterMixin from '../SvgFilterMixin.vue'
142+
import DaemonSelectionDialog from '../DaemonSelectionDialog/DaemonSelectionDialog.vue'
144143
import AppLevelBadge from './AppLevelBadge.vue'
145144
import AppScore from './AppScore.vue'
146145
import AppManagement from '../../mixins/AppManagement.js'
147-
import { useAppApiStore } from '../../store/app-api-store.ts'
148-
import { useAppsStore } from '../../store/apps-store.js'
146+
import { useAppsStore } from '../../store/apps.ts'
147+
import { useAppApiStore } from '../../store/exApps.ts'
149148
150149
export default {
151150
name: 'AppItem',
@@ -157,7 +156,7 @@ export default {
157156
DaemonSelectionDialog,
158157
},
159158
160-
mixins: [AppManagement, SvgFilterMixin],
159+
mixins: [AppManagement],
161160
props: {
162161
app: {
163162
type: Object,
@@ -281,7 +280,6 @@ export default {
281280
</script>
282281
283282
<style scoped lang="scss">
284-
@use '../../../../../core/css/variables.scss' as variables;
285283
@use 'sass:math';
286284
287285
.app-item {
@@ -364,7 +362,7 @@ export default {
364362
}
365363
366364
/* Hide actions on a small screen. Click on app opens fill-screen sidebar with the buttons */
367-
@media only screen and (max-width: math.div(variables.$breakpoint-mobile, 2)) {
365+
@media only screen and (max-width: 512px) {
368366
.app-actions {
369367
display: none;
370368
}
@@ -437,7 +435,7 @@ export default {
437435
}
438436
}
439437
440-
@media only screen and (max-width: variables.$breakpoint-mobile) {
438+
@media only screen and (max-width: 1024px) {
441439
width: 50%;
442440
}
443441

apps/appstore/src/components/AppList/AppScore.vue renamed to apps/appstore/src/components/AppScore.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export default defineComponent({
5353
computed: {
5454
title() {
5555
const appScore = (this.score * 5).toFixed(1)
56-
return t('settings', 'Community rating: {score}/5', { score: appScore })
56+
return t('appstore', 'Community rating: {score}/5', { score: appScore })
5757
},
5858
5959
fullStars() {

0 commit comments

Comments
 (0)