]> source.dussan.org Git - nextcloud-server.git/commitdiff
refactor(appstore): Split `AppItemIcon` to make the `AppItem` component better mainta...
authorFerdinand Thiessen <opensource@fthiessen.de>
Fri, 18 Oct 2024 18:30:28 +0000 (20:30 +0200)
committerFerdinand Thiessen <opensource@fthiessen.de>
Wed, 23 Oct 2024 10:58:52 +0000 (12:58 +0200)
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
apps/settings/src/app-types.ts
apps/settings/src/components/AppList/AppItem.vue
apps/settings/src/components/AppStore/AppItem/AppItemIcon.vue [new file with mode: 0644]
apps/settings/src/service/imagePreloading.ts [new file with mode: 0644]

index 604e250df3d924057cf1db0a650a1bf07dd399b0..22c290b835f59f6a78ba9e396fa01f8033521293 100644 (file)
@@ -39,6 +39,8 @@ export interface IAppstoreApp {
        category: string|string[]
 
        preview?: string
+       /** The preview is an icon */
+       previewAsIcon: boolean
        screenshot?: string
 
        active: boolean
@@ -48,6 +50,10 @@ export interface IAppstoreApp {
        canInstall: boolean
        canUninstall: boolean
        isCompatible: boolean
+       /** Available version to update to */
+       update?: string
+
+       score: number
 
        appstoreData: Record<string, never>
        releases?: IAppstoreAppRelease[]
index 08faa06f1cd765cd2f561f9563a7e36221dc8a87..32c06317248b62a559a6b055fa11eccf4763da0c 100644 (file)
                        'app-item--selected': isSelected,
                        'app-item--with-sidebar': withSidebar,
                }">
-               <component :is="dataItemTag"
-                       class="app-image app-image-icon"
-                       :headers="getDataItemHeaders(`app-table-col-icon`)">
-                       <div v-if="(listView && !app.preview) || (!listView && !screenshotLoaded)" class="icon-settings-dark" />
-
-                       <svg v-else-if="listView && app.preview"
-                               width="32"
-                               height="32"
-                               viewBox="0 0 32 32">
-                               <image x="0"
-                                       y="0"
-                                       width="32"
-                                       height="32"
-                                       preserveAspectRatio="xMinYMin meet"
-                                       :xlink:href="app.preview"
-                                       class="app-icon" />
-                       </svg>
-
-                       <img v-if="!listView && app.screenshot && screenshotLoaded" :src="app.screenshot" alt="">
-               </component>
+               <AppItemIcon :app="app"
+                       :list-view="listView"
+                       :headers="useBundleView ? `${headers} app-table-col-icon` : undefined" />
                <component :is="dataItemTag"
                        class="app-name"
                        :headers="getDataItemHeaders(`app-table-col-name`)">
diff --git a/apps/settings/src/components/AppStore/AppItem/AppItemIcon.vue b/apps/settings/src/components/AppStore/AppItem/AppItemIcon.vue
new file mode 100644 (file)
index 0000000..44828a4
--- /dev/null
@@ -0,0 +1,103 @@
+<!--
+ - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
+
+<script setup lang="ts">
+import type { IAppstoreApp } from '../../../app-types'
+import { computed, ref, watchEffect } from 'vue'
+import IconSettings from 'vue-material-design-icons/Cog.vue'
+
+import { preloadImage } from '../../../service/imagePreloading.ts'
+
+const props = defineProps<{
+       app: IAppstoreApp
+       listView: boolean
+}>()
+
+/**
+ * The preview URL to use.
+ * For list view we always force the preview (e.g. the app icon), for grid view we prefer the screenshot but fallback to preview.
+ */
+const previewUrl = computed(() => props.listView ? props.app.preview : (props.app.screenshot ?? props.app.preview))
+
+/**
+ * Is the shown image an icon.
+ * This is the case if `previewAsIcon` is set
+ *   AND either we are in list view where the preview is shown
+ *   OR no screenshot is available and we fallback to the preview.
+ */
+const isPreviewIcon = computed(() => props.app.previewAsIcon && (props.listView || !props.app.screenshot))
+
+/**
+ * True if a preview is available.
+ * For list view we use the preview (which is the icon), for grid view we use the screenshot.
+ */
+const hasPreview = computed(() => Boolean(previewUrl.value))
+
+/**
+ * Preload the preview until it is loaded show the placeholder
+ */
+const previewLoaded = ref(false)
+watchEffect(() => {
+       previewLoaded.value = false
+       if (hasPreview.value) {
+               preloadImage(previewUrl.value!)
+                       .then(() => { previewLoaded.value = true })
+       }
+})
+
+/**
+ * The HTML tag to use - depending on the list vs grid view
+ */
+const tag = computed(() => props.listView ? 'td' : 'div')
+</script>
+
+<template>
+       <component :is="tag" class="app-item-icon" :class="{ 'app-item-icon--grid': !listView }">
+               <IconSettings v-if="!hasPreview || !previewLoaded"
+                       class="app-item-icon__fallback"
+                       :size="listView ? 20 : 64" />
+               <img v-else
+                       alt=""
+                       class="app-item-icon__image"
+                       :class="{ 'app-item-icon__image--is-icon': isPreviewIcon }"
+                       :src="previewUrl">
+       </component>
+</template>
+
+<style scoped lang="scss">
+.app-item-icon {
+       height: auto;
+       width: var(--default-clickable-area);
+       position: relative;
+       overflow: hidden;
+
+       .app-item-icon__fallback {
+               color: var(--color-text-maxcontrast);
+       }
+
+       .app-item-icon__image {
+               height: 20px;
+               width: 20px;
+
+               &--is-icon {
+                       // if an icon is shown we need to adjust the color if needed
+                       filter: var(--background-invert-if-bright);
+                       opacity: 0.6;
+               }
+       }
+
+       &--grid {
+               height: 150px;
+               width: auto;
+
+               .app-item-icon__image,
+               .app-item-icon__fallback {
+                       height: 150px;
+                       width: 100%;
+                       object-fit: cover;
+               }
+       }
+}
+</style>
diff --git a/apps/settings/src/service/imagePreloading.ts b/apps/settings/src/service/imagePreloading.ts
new file mode 100644 (file)
index 0000000..2bee13c
--- /dev/null
@@ -0,0 +1,24 @@
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+import PQueue from 'p-queue'
+
+const queue = new PQueue({ concurrency: 5 })
+
+/**
+ * Preload a given image URL, the requests are limited to a specific concurrency to not overload any host.
+ * @param url The image URL to preload
+ */
+export function preloadImage(url: string): Promise<void> {
+       return queue.add(async () => {
+               const { promise, resolve, reject } = Promise.withResolvers()
+
+               const img = new Image()
+               img.onload = resolve
+               img.onerror = reject
+               img.src = url
+
+               await promise
+       })
+}