mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-28 04:25:03 +02:00
UBERF-9480 Use image previews of proper size (#8008)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
@@ -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}
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user