+++ /dev/null
-<!--
- - SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
- - SPDX-License-Identifier: AGPL-3.0-or-later
--->
-
-<template>
- <!-- Apps list -->
- <NcAppContent class="app-settings-content"
- :page-heading="appStoreLabel">
- <h2 class="app-settings-content__label" v-text="viewLabel" />
-
- <AppStoreDiscoverSection v-if="currentCategory === 'discover'" />
- <NcEmptyContent v-else-if="isLoading"
- class="empty-content__loading"
- :name="t('settings', 'Loading app list')">
- <template #icon>
- <NcLoadingIcon :size="64" />
- </template>
- </NcEmptyContent>
- <AppList v-else :category="currentCategory" />
- </NcAppContent>
-</template>
-
-<script setup lang="ts">
-import { translate as t } from '@nextcloud/l10n'
-import { computed, getCurrentInstance, onBeforeMount, watchEffect } from 'vue'
-import { useRoute } from 'vue-router/composables'
-
-import { useAppsStore } from '../store/apps-store'
-import { APPS_SECTION_ENUM } from '../constants/AppsConstants'
-
-import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
-import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
-import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
-import AppList from '../components/AppList.vue'
-import AppStoreDiscoverSection from '../components/AppStoreDiscover/AppStoreDiscoverSection.vue'
-
-const route = useRoute()
-const store = useAppsStore()
-
-/**
- * ID of the current active category, default is `discover`
- */
-const currentCategory = computed(() => route.params?.category ?? 'discover')
-
-const appStoreLabel = t('settings', 'App Store')
-const viewLabel = computed(() => APPS_SECTION_ENUM[currentCategory.value] ?? store.getCategoryById(currentCategory.value)?.displayName ?? appStoreLabel)
-
-watchEffect(() => {
- window.document.title = `${viewLabel.value} - ${appStoreLabel} - Nextcloud`
-})
-
-// TODO this part should be migrated to pinia
-const instance = getCurrentInstance()
-/** Is the app list loading */
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const isLoading = computed(() => (instance?.proxy as any).$store.getters.loading('list'))
-onBeforeMount(() => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (instance?.proxy as any).$store.dispatch('getCategories', { shouldRefetchCategories: true });
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- (instance?.proxy as any).$store.dispatch('getAllApps')
-})
-</script>
-
-<style scoped>
-.empty-content__loading {
- height: 100%;
-}
-
-.app-settings-content__label {
- margin-block-start: var(--app-navigation-padding);
- margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);
- min-height: var(--default-clickable-area);
- line-height: var(--default-clickable-area);
- vertical-align: center;
-}
-</style>
--- /dev/null
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<template>
+ <!-- Apps list -->
+ <NcAppContent :page-heading="appStoreLabel">
+ <h2 class="app-store__label" v-text="viewLabel" />
+ <router-view />
+ </NcAppContent>
+</template>
+
+<script setup lang="ts">
+import { t } from '@nextcloud/l10n'
+import { computed, onBeforeMount, watchEffect } from 'vue'
+import { useRoute } from 'vue-router/composables'
+
+import { useAppStore } from '../../store/appStore'
+import { AppStoreSectionNames } from '../../constants/AppStoreConstants'
+
+import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
+
+const route = useRoute()
+const store = useAppStore()
+
+/**
+ * ID of the current active category
+ */
+const currentCategory = computed(() => route.params?.category ?? route.name!)
+
+const appStoreLabel = t('settings', 'App Store')
+const viewLabel = computed(() => AppStoreSectionNames[currentCategory.value]
+ ?? store.getCategoryById(currentCategory.value)?.displayName
+ ?? appStoreLabel,
+)
+
+// Update the window title based on the current category
+watchEffect(() => {
+ window.document.title = `${viewLabel.value} - ${appStoreLabel} - Nextcloud`
+})
+// Load apps as both discover and normal app view require them to be loaded
+onBeforeMount(() => store.loadApps())
+onBeforeMount(() => store.loadCategories())
+</script>
+
+<style scoped>
+.app-store__label {
+ margin-block-start: var(--app-navigation-padding);
+ margin-inline-start: calc(var(--default-clickable-area) + var(--app-navigation-padding) * 2);
+ min-height: var(--default-clickable-area);
+ line-height: var(--default-clickable-area);
+ font-size: calc(var(--default-clickable-area) / 1.5);
+}
+</style>
--- /dev/null
+<!--
+ - SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+<script setup lang="ts">
+import { computed } from 'vue'
+import { useCurrentCategory } from '../../composables/useCurrentCategory'
+import { AppStoreListViewCategories } from '../../constants/AppStoreConstants'
+import { useAppStore } from '../../store/appStore'
+import { useAppStoreSearchStore } from '../../store/appStoreSearch'
+
+import AppStoreViewGrid from './AppStoreViewGrid.vue'
+import AppStoreViewList from './AppStoreViewList.vue'
+import AppStoreViewLoading from './AppStoreViewLoading.vue'
+import AppStoreViewNotFound from './AppStoreViewNotFound.vue'
+import AppStoreViewNoResults from './AppStoreViewNoResults.vue'
+
+const store = useAppStore()
+const searchStore = useAppStoreSearchStore()
+const {
+ currentCategory,
+ currentCategoryName,
+} = useCurrentCategory()
+
+/** True if the category could not be found */
+const invalidCategory = computed(() => currentCategoryName.value === undefined)
+/** True if the current category should be displayed in a list-view */
+const isListView = computed(() => AppStoreListViewCategories.includes(currentCategory.value))
+
+/**
+ * True if apps / categories are currently loading (being fetched)
+ */
+const isLoading = computed(() => store.loading.categories || store.loading.apps)
+/**
+ * True if there are no search results for the current query
+ */
+const noSearchResults = computed(() => searchStore.query !== '' && searchStore.searchResults.length === 0)
+</script>
+
+<template>
+ <AppStoreViewLoading v-if="isLoading" />
+ <AppStoreViewNotFound v-else-if="invalidCategory" />
+ <AppStoreViewNoResults v-else-if="noSearchResults" />
+ <AppStoreViewList v-else-if="isListView" />
+ <AppStoreViewGrid v-else />
+</template>
--- /dev/null
+<script setup lang="ts">
+import { computed, ref } from 'vue'
+import { useElementSize } from '@vueuse/core'
+import { useCurrentCategory } from '../../composables/useCurrentCategory'
+import { useAppStoreSearchStore } from '../../store/appStoreSearch'
+import { useAppStore } from '../../store/appStore'
+import AppItem from '../../components/AppStore/AppItem/AppItem.vue'
+
+const store = useAppStore()
+const searchStore = useAppStoreSearchStore()
+const { currentCategory } = useCurrentCategory()
+
+const apps = computed(() => store.getAppsByCategory(currentCategory.value).filter(searchStore.filterAppsByQuery))
+
+const listElement = ref<HTMLUListElement>()
+const { width: containerWidth } = useElementSize(listElement)
+
+const itemWidth = computed(() => {
+ if (containerWidth.value >= 1400) {
+ return '25%'
+ } else if (containerWidth.value >= 900) {
+ return '33%'
+ } else if (containerWidth.value >= 600) {
+ return '50%'
+ } else {
+ return '100%'
+ }
+})
+</script>
+
+<template>
+ <TransitionGroup ref="listElement"
+ name="grid-transition"
+ tag="ul"
+ class="app-store-view-grid">
+ <AppItem v-for="app in apps"
+ :key="app.id"
+ :app="app"
+ :category="currentCategory"
+ class="app-store-view-grid__item" />
+ </TransitionGroup>
+</template>
+
+<style scoped lang="scss">
+.grid-transition-enter-active, .grid-transition-leave-active {
+ transition: all var(--animation-slow);
+}
+.grid-transition-enter, .grid-transition-leave-to {
+ opacity: 0;
+ transform: scale(.8);
+}
+
+.app-store-view-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--default-grid-baseline);
+ padding: var(--app-navigation-padding);
+ width: 100%;
+
+ &__item {
+ width: calc(v-bind('itemWidth') - var(--default-grid-baseline));
+ }
+}
+</style>
--- /dev/null
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<script setup lang="ts">
+import { t } from '@nextcloud/l10n'
+import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js'
+</script>
+
+<template>
+ <NcEmptyContent class="app-store-loading"
+ :name="t('settings', 'Loading app list')">
+ <template #icon>
+ <NcLoadingIcon :size="64" />
+ </template>
+ </NcEmptyContent>
+</template>
+
+<style scoped>
+.app-store-loading {
+ height: 100%;
+}
+</style>
--- /dev/null
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<script setup lang="ts">
+import { t } from '@nextcloud/l10n'
+import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
+import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js'
+import IconMagnify from 'vue-material-design-icons/Magnify.vue'
+</script>
+
+<template>
+ <NcEmptyContent class="app-store-not-found"
+ :name="t('settings', 'Category not found')">
+ <template #icon>
+ <IconMagnify :size="64" />
+ </template>
+ <template #action>
+ <NcButton type="primary" :to="{ name: 'apps' }">
+ {{ t('settings', 'Go back') }}
+ </NcButton>
+ </template>
+ </NcEmptyContent>
+</template>
+
+<style scoped>
+.app-store-not-found {
+ height: 100%;
+}
+</style>