UBERF-9480 Use image previews of proper size (#8008)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov
2025-02-14 20:03:21 +07:00
committed by GitHub
parent de7a032a9f
commit f1d0817984
13 changed files with 238 additions and 82 deletions
+1 -1
View File
@@ -31,7 +31,7 @@
"MODEL_VERSION": "",
"PREVIEW_CONFIG": "https://dl.hc.engineering/image/fit=scale-down,width=:size/:workspace/:blobId",
"PREVIEW_CONFIG": "image|https://dl.hc.engineering/image/fit=scale-down,width=:width,height=:height,dpr=:dpr/:workspace/:blobId;video|https://dl.hc.engineering/video/:workspace/:blobId/meta",
"PRINT_URL": "https://print.hc.engineering",
@@ -0,0 +1,39 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import type { Blob, Ref } from '@hcengineering/core'
import { Image } from '@hcengineering/ui'
import { getBlobRef } from '../preview'
export let blob: Ref<Blob>
export let alt: string = ''
export let fit: string = 'contain'
export let width: number
export let height: number
export let responsive: boolean = false
</script>
{#await getBlobRef(blob, alt, width, height) then blobSrc}
<Image
src={blobSrc.src}
srcset={blobSrc.srcset}
{alt}
width={responsive ? '100%' : width}
height={responsive ? '100%' : height}
{fit}
on:load
on:error
/>
{/await}
+1
View File
@@ -50,6 +50,7 @@ export { default as DownloadFileButton } from './components/DownloadFileButton.s
export { default as FileTypeIcon } from './components/FileTypeIcon.svelte'
export { default as DrawingBoard } from './components/DrawingBoard.svelte'
export { default as DrawingBoardToolbar } from './components/DrawingBoardToolbar.svelte'
export { default as Image } from './components/Image.svelte'
export { default } from './plugin'
export * from './types'
export * from './utils'
+26 -13
View File
@@ -63,26 +63,32 @@ export function getPreviewConfig (): PreviewConfig {
export async function getBlobRef (
file: Ref<Blob>,
name?: string,
width?: number
width?: number,
height?: number
): Promise<{
src: string
srcset: string
}> {
return {
src: getFileUrl(file, name),
srcset: getSrcSet(file, width)
srcset: getSrcSet(file, width, height)
}
}
export async function getBlobSrcSet (file: Ref<Blob>, width?: number): Promise<string> {
return getSrcSet(file, width)
export async function getBlobSrcSet (file: Ref<Blob>, width?: number, height?: number): Promise<string> {
return getSrcSet(file, width, height)
}
export function getSrcSet (_blob: Ref<Blob>, width?: number): string {
return blobToSrcSet(getPreviewConfig(), _blob, width)
export function getSrcSet (_blob: Ref<Blob>, width?: number, height?: number): string {
return blobToSrcSet(getPreviewConfig(), _blob, width, height)
}
function blobToSrcSet (cfg: PreviewConfig, blob: Ref<Blob>, width: number | undefined): string {
function blobToSrcSet (
cfg: PreviewConfig,
blob: Ref<Blob>,
width: number | undefined,
height: number | undefined
): string {
if (blob.includes('://')) {
return ''
}
@@ -98,25 +104,32 @@ function blobToSrcSet (cfg: PreviewConfig, blob: Ref<Blob>, width: number | unde
url = url.replaceAll(':blobId', encodeURIComponent(blob))
let result = ''
const fu = url
if (width !== undefined) {
result +=
fu.replaceAll(':size', `${width}`) +
formatImageSize(url, width, height ?? width, 1) +
' 1x , ' +
fu.replaceAll(':size', `${width * 2}`) +
formatImageSize(url, width, height ?? width, 2) +
' 2x, ' +
fu.replaceAll(':size', `${width * 3}`) +
formatImageSize(url, width, height ?? width, 3) +
' 3x'
}
return result
}
function formatImageSize (url: string, width: number, height: number, dpr: number): string {
return url
.replaceAll(':size', `${width * dpr}`)
.replaceAll(':width', `${width}`)
.replaceAll(':height', `${height}`)
.replaceAll(':dpr', `${dpr}`)
}
/***
* @deprecated, please use Blob direct operations.
*/
export function getFileSrcSet (_blob: Ref<Blob>, width?: number): string {
return blobToSrcSet(getPreviewConfig(), _blob, width)
export function getFileSrcSet (_blob: Ref<Blob>, width?: number, height?: number): string {
return blobToSrcSet(getPreviewConfig(), _blob, width, height)
}
/**
+11 -6
View File
@@ -506,6 +506,11 @@ export function getCurrentWorkspaceUrl (): string {
return wsId
}
export function remToPx (rem: number): number {
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
return rem * fontSize
}
export function sizeToWidth (size: string): number | undefined {
let width: number | undefined
switch (size) {
@@ -515,22 +520,22 @@ export function sizeToWidth (size: string): number | undefined {
case 'x-small':
case 'smaller':
case 'small':
width = 32
width = 2
break
case 'medium':
width = 64
width = 2.5
break
case 'large':
width = 256
width = 4.5
break
case 'x-large':
width = 512
width = 7.5
break
case '2x-large':
width = 1024
width = 10
break
}
return width
return width !== undefined ? remToPx(width) : undefined
}
/**
+56
View File
@@ -0,0 +1,56 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
export let src: string
export let srcset: string | undefined = undefined
export let alt: string = ''
export let width: number | string
export let height: number | string
export let fit: string = 'contain'
const dispatch = createEventDispatcher()
let retry: number = 0
$: if (src !== undefined) {
retry = 0
}
function handleLoad (): void {
retry = 0
dispatch('load')
}
function handleError (event: any): void {
if (retry > 3) {
event.target.src = undefined
} else if (retry > 0) {
event.target.src = `${src}#${Date.now()}`
retry -= 1
} else {
dispatch('error')
}
}
</script>
<img {src} {srcset} {alt} {width} {height} style:object-fit={fit} on:error={handleError} on:load={handleLoad} />
<style lang="scss">
img {
border-radius: inherit;
}
</style>
+1
View File
@@ -285,6 +285,7 @@ export { default as NestedMenu } from './components/NestedMenu.svelte'
export { default as NestedDropdown } from './components/NestedDropdown.svelte'
export { default as Dock } from './components/Dock.svelte'
export { default as Image } from './components/Image.svelte'
export * from './types'
export * from './location'
@@ -23,7 +23,7 @@
export let value: Doc & { attachments?: number }
export let attachments: Attachment[] | undefined = undefined
export let imageSize: AttachmentImageSize = 'auto'
export let imageSize: AttachmentImageSize = 'x-large'
export let videoPreload = true
export let isOwn = false
@@ -14,18 +14,19 @@
-->
<script lang="ts">
import type { Attachment } from '@hcengineering/attachment'
import { getBlobRef, sizeToWidth } from '@hcengineering/presentation'
import { IconSize } from '@hcengineering/ui'
import type { WithLookup } from '@hcengineering/core'
import { Image } from '@hcengineering/presentation'
import { Loading } from '@hcengineering/ui'
import BrokenImage from './icons/BrokenImage.svelte'
import { AttachmentImageSize } from '../types'
export let value: WithLookup<Attachment>
export let size: AttachmentImageSize = 'auto'
interface Dimensions {
width: 'auto' | number
height: 'auto' | number
width: number
height: number
fit: 'cover' | 'contain'
}
@@ -37,16 +38,14 @@
} as const
let dimensions: Dimensions
let urlSize: IconSize
$: dimensions = getDimensions(value, size)
$: urlSize = getUrlSize(size)
function getDimensions (value: Attachment, size: AttachmentImageSize): Dimensions {
if (size === 'auto') {
return {
width: 'auto',
height: 'auto',
width: 300,
height: 300,
fit: 'contain'
}
}
@@ -54,7 +53,7 @@
const preferredWidth = preferredWidthMap[size]
const { metadata } = value
if (!metadata) {
if (metadata === undefined) {
return {
width: preferredWidth,
height: preferredWidth,
@@ -69,7 +68,7 @@
const width = Math.min(originalWidth, preferredWidth)
const ratio = originalHeight / originalWidth
const height = width * ratio
const height = Math.ceil(width * ratio)
const fit = width < minSize || height < minSize ? 'cover' : 'contain'
@@ -94,42 +93,66 @@
}
}
function getUrlSize (size: AttachmentImageSize): IconSize {
if (size === 'auto') {
return 'large'
}
return 'x-large'
}
function toStyle (size: 'auto' | number): string {
return size === 'auto' ? 'auto' : `${size}px`
}
let loading = false
let error = false
$: if (value !== undefined) {
loading = true
}
function handleLoad (): void {
loading = false
}
function handleError (): void {
loading = false
error = true
}
</script>
<div class="container" style="width:{toStyle(dimensions.width)}; height:{toStyle(dimensions.height)}">
{#await getBlobRef(value.file, value.name, sizeToWidth(urlSize)) then blobSrc}
<img
src={blobSrc.src}
style:object-fit={dimensions.fit}
width="100%"
height="100%"
srcset={blobSrc.srcset}
alt={value.name}
/>
{/await}
<div class="container" class:loading style="width:{toStyle(dimensions.width)}; height:{toStyle(dimensions.height)}">
{#if loading}
<div class="image-overlay">
<Loading />
</div>
{/if}
{#if error}
<div class="image-overlay">
<BrokenImage size={'large'} />
</div>
{/if}
<Image
blob={value.file}
alt={value.name}
fit={dimensions.fit}
width={dimensions.width}
height={dimensions.height}
on:load={handleLoad}
on:error={handleError}
/>
</div>
<style lang="scss">
img {
max-width: 20rem;
max-height: 20rem;
border-radius: 0.75rem;
min-height: 4rem;
min-width: 4rem;
}
.container {
display: inline-flex;
background-color: var(--theme-link-preview-bg-color);
border-radius: 0.75rem;
.image-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
}
</style>
@@ -0,0 +1,25 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
export let size: 'small' | 'medium' | 'large'
const fill: string = 'currentColor'
</script>
<svg class="svg-{size}" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path
{fill}
d="M21 5v6.59l-3-3.01-4 4.01-4-4-4 4-3-3.01V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2zm-3 6.42l3 3.01V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-6.58l3 2.99 4-4 4 4 4-4z"
/>
</svg>
@@ -47,7 +47,7 @@
export let inline = false
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
export let withShowMore: boolean = true
export let attachmentImageSize: AttachmentImageSize = 'auto'
export let attachmentImageSize: AttachmentImageSize = 'x-large'
export let videoPreload = true
export let hideLink = false
export let compact = false
@@ -66,7 +66,7 @@
}}
>
<div class="card-content">
<Thumbnail {object} size={'x-large'} />
<Thumbnail {object} />
</div>
<div class="header flex-col p-2 pt-1">
@@ -15,14 +15,14 @@
<script lang="ts">
import { type WithLookup } from '@hcengineering/core'
import drive, { type Resource } from '@hcengineering/drive'
import { getBlobRef, getClient, sizeToWidth } from '@hcengineering/presentation'
import { Icon, IconSize } from '@hcengineering/ui'
import { Image, getClient, remToPx } from '@hcengineering/presentation'
import { Icon } from '@hcengineering/ui'
import IconFolderThumbnail from './icons/FolderThumbnail.svelte'
export let object: WithLookup<Resource>
export let size: IconSize = 'x-large'
const size = remToPx(20)
const client = getClient()
const hierarchy = client.getHierarchy()
@@ -44,18 +44,17 @@
{#if isFolder}
<Icon icon={IconFolderThumbnail} size={'full'} fill={'var(--global-no-priority-PriorityColor)'} />
{:else if previewRef != null && isImage && !isError}
{#await getBlobRef(previewRef, object.title, sizeToWidth(size)) then blobSrc}
<img
draggable="false"
class="img-fit"
src={blobSrc.src}
srcset={blobSrc.srcset}
alt={object.title}
on:error={() => {
isError = true
}}
/>
{/await}
<Image
blob={previewRef}
alt={object.title}
width={size}
height={size}
responsive
fit={'cover'}
on:error={() => {
isError = true
}}
/>
{:else}
<div class="flex-center ext-icon">
{extensionIconLabel(object.title)}
@@ -74,10 +73,4 @@
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 0.5rem;
}
.img-fit {
object-fit: cover;
height: 100%;
width: 100%;
}
</style>