mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
UBERF-5595: set up attachments sizes (#4746)
Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
parent
0349cec3a2
commit
3e782658c6
@ -20609,12 +20609,13 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/presentation.tgz(@types/node@20.11.19)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-aIs1NWqMRjF8+bpEPUJW3nkQFkxnv0GvllOQk4DWbDdfxuAlGkvBKiKaDwlx3cku9hulZqpwr6EyIbkotn1Rsw==, tarball: file:projects/presentation.tgz}
|
||||
resolution: {integrity: sha512-afwb+Kuc6Gu/8xgzxYSMNmMewCvsAX46bAkIMVDBO71momZ1zBKAhLM1kFXIp1UnaOKH2vnq3myx4ZpOrjBAZw==, tarball: file:projects/presentation.tgz}
|
||||
id: file:projects/presentation.tgz
|
||||
name: '@rush-temp/presentation'
|
||||
version: 0.0.0
|
||||
dependencies:
|
||||
'@types/jest': 29.5.12
|
||||
'@types/png-chunks-extract': 1.0.2
|
||||
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.3.3)
|
||||
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3)
|
||||
eslint: 8.56.0
|
||||
@ -20625,6 +20626,7 @@ packages:
|
||||
eslint-plugin-svelte: 2.35.1(eslint@8.56.0)(svelte@4.2.11)(ts-node@10.9.2)
|
||||
fast-equals: 2.0.4
|
||||
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
|
||||
png-chunks-extract: 1.0.0
|
||||
prettier: 3.2.5
|
||||
prettier-plugin-svelte: 3.2.1(prettier@3.2.5)(svelte@4.2.11)
|
||||
sass: 1.71.1
|
||||
@ -23328,7 +23330,7 @@ packages:
|
||||
dev: false
|
||||
|
||||
file:projects/text-editor.tgz(@types/node@20.11.19)(bufferutil@4.0.8)(esbuild@0.20.1)(postcss-load-config@4.0.2)(postcss@8.4.35)(prosemirror-model@1.19.4)(ts-node@10.9.2):
|
||||
resolution: {integrity: sha512-/TK8U02uYlCsPKiYVNS7t+UMmDxEPWLwY05soaV+Yuu3nXRGt2m2CkEFnv8I5IAqsEzgogKhqFXAgcQHcjfG+A==, tarball: file:projects/text-editor.tgz}
|
||||
resolution: {integrity: sha512-obPE9MHV6S63InShtdrvDbNa5/A9BgF49k6zgO3T4C+niVTKsmsKz4/Pt+aew2X5eAAxehSq69bWIjBZidydNA==, tarball: file:projects/text-editor.tgz}
|
||||
id: file:projects/text-editor.tgz
|
||||
name: '@rush-temp/text-editor'
|
||||
version: 0.0.0
|
||||
|
@ -14,7 +14,7 @@
|
||||
//
|
||||
|
||||
import activity from '@hcengineering/activity'
|
||||
import type { Attachment, Photo, SavedAttachments } from '@hcengineering/attachment'
|
||||
import type { Attachment, AttachmentMetadata, Photo, SavedAttachments } from '@hcengineering/attachment'
|
||||
import { type Domain, IndexKind, type Ref } from '@hcengineering/core'
|
||||
import {
|
||||
type Builder,
|
||||
@ -64,6 +64,8 @@ export class TAttachment extends TAttachedDoc implements Attachment {
|
||||
|
||||
@Prop(TypeBoolean(), attachment.string.Pinned)
|
||||
pinned!: boolean
|
||||
|
||||
metadata?: AttachmentMetadata
|
||||
}
|
||||
|
||||
@Model(attachment.class.Photo, attachment.class.Attachment)
|
||||
|
@ -35,7 +35,8 @@
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"svelte-eslint-parser": "^0.33.1"
|
||||
"svelte-eslint-parser": "^0.33.1",
|
||||
"@types/png-chunks-extract": "^1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/platform": "^0.6.9",
|
||||
@ -46,6 +47,7 @@
|
||||
"svelte": "^4.2.5",
|
||||
"@hcengineering/client": "^0.6.14",
|
||||
"@hcengineering/collaborator-client": "^0.6.0",
|
||||
"fast-equals": "^2.0.3"
|
||||
"fast-equals": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
101
packages/presentation/src/image.ts
Normal file
101
packages/presentation/src/image.ts
Normal file
@ -0,0 +1,101 @@
|
||||
//
|
||||
// Copyright © 2024 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.
|
||||
//
|
||||
|
||||
import extract from 'png-chunks-extract'
|
||||
|
||||
export async function getImageSize (
|
||||
file: File,
|
||||
src: string
|
||||
): Promise<{ width: number, height: number, pixelRatio: number }> {
|
||||
const size = isPng(file) ? await getPngImageSize(file) : undefined
|
||||
|
||||
const promise = new Promise<{ width: number, height: number, pixelRatio: number }>((resolve, reject) => {
|
||||
const img = new Image()
|
||||
|
||||
img.onload = () => {
|
||||
resolve({
|
||||
width: size?.width ?? img.naturalWidth,
|
||||
height: size?.height ?? img.naturalHeight,
|
||||
pixelRatio: size?.pixelRatio ?? 1
|
||||
})
|
||||
}
|
||||
|
||||
img.onerror = reject
|
||||
img.src = src
|
||||
})
|
||||
|
||||
return await promise
|
||||
}
|
||||
|
||||
function isPng (file: File): boolean {
|
||||
return file.type === 'image/png'
|
||||
}
|
||||
|
||||
async function getPngImageSize (file: File): Promise<{ width: number, height: number, pixelRatio: number } | undefined> {
|
||||
if (!isPng(file)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await file.arrayBuffer()
|
||||
const chunks = extract(new Uint8Array(buffer))
|
||||
|
||||
const pHYsChunk = chunks.find((chunk) => chunk.name === 'pHYs')
|
||||
const iHDRChunk = chunks.find((chunk) => chunk.name === 'IHDR')
|
||||
|
||||
if (pHYsChunk === undefined || iHDRChunk === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
|
||||
// Section 4.1.1. IHDR Image header
|
||||
// Section 4.2.4.2. pHYs Physical pixel dimensions
|
||||
const idhrData = parseIHDR(new DataView(iHDRChunk.data.buffer))
|
||||
const physData = parsePhys(new DataView(pHYsChunk.data.buffer))
|
||||
|
||||
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
|
||||
const pixelRatio = Math.round(physData.ppux / 2834.5)
|
||||
return {
|
||||
width: idhrData.width,
|
||||
height: idhrData.height,
|
||||
pixelRatio
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
|
||||
// Section 4.1.1. IHDR Image header
|
||||
function parseIHDR (view: DataView): { width: number, height: number } {
|
||||
return {
|
||||
width: view.getUint32(0),
|
||||
height: view.getUint32(4)
|
||||
}
|
||||
}
|
||||
|
||||
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
|
||||
// Section 4.2.4.2. pHYs Physical pixel dimensions
|
||||
function parsePhys (view: DataView): { ppux: number, ppuy: number, unit: number } {
|
||||
return {
|
||||
ppux: view.getUint32(0),
|
||||
ppuy: view.getUint32(4),
|
||||
unit: view.getUint8(4)
|
||||
}
|
||||
}
|
@ -56,3 +56,4 @@ export * from './pipeline'
|
||||
export * from './components/extensions/manager'
|
||||
export * from './rules'
|
||||
export * from './search'
|
||||
export * from './image'
|
||||
|
@ -34,8 +34,7 @@
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"svelte-eslint-parser": "^0.33.1",
|
||||
"@types/png-chunks-extract": "^1.0.2"
|
||||
"svelte-eslint-parser": "^0.33.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hcengineering/presentation": "^0.6.2",
|
||||
@ -80,7 +79,6 @@
|
||||
"rfc6902": "^5.0.1",
|
||||
"diff": "^5.1.0",
|
||||
"slugify": "^1.6.6",
|
||||
"lib0": "^0.2.88",
|
||||
"png-chunks-extract": "^1.0.0"
|
||||
"lib0": "^0.2.88"
|
||||
}
|
||||
}
|
||||
|
@ -12,14 +12,14 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
import { PDFViewer } from '@hcengineering/presentation'
|
||||
import { PDFViewer, getImageSize } from '@hcengineering/presentation'
|
||||
import { ImageNode, type ImageOptions as ImageNodeOptions } from '@hcengineering/text'
|
||||
import { type IconSize, getIconSize2x, showPopup } from '@hcengineering/ui'
|
||||
import { mergeAttributes, nodeInputRule } from '@tiptap/core'
|
||||
import { type Node as ProseMirrorNode } from '@tiptap/pm/model'
|
||||
import { Plugin, PluginKey } from '@tiptap/pm/state'
|
||||
import { type EditorView } from '@tiptap/pm/view'
|
||||
import extract from 'png-chunks-extract'
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@ -318,76 +318,27 @@ async function handleImageUpload (
|
||||
attachFile: FileAttachFunction,
|
||||
uploadUrl: string
|
||||
): Promise<void> {
|
||||
const size = await getImageSize(file)
|
||||
const attached = await attachFile(file)
|
||||
if (attached !== undefined) {
|
||||
if (attached.type.includes('image')) {
|
||||
const image = new Image()
|
||||
image.onload = () => {
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': attached.file,
|
||||
width: size?.width ?? image.naturalWidth
|
||||
})
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
view.dispatch(transaction)
|
||||
}
|
||||
image.src = getFileUrl(attached.file, 'full', uploadUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getImageSize (file: File): Promise<{ width: number, height: number } | undefined> {
|
||||
if (file.type !== 'image/png') {
|
||||
return undefined
|
||||
if (attached === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!attached.type.includes('image')) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const buffer = await file.arrayBuffer()
|
||||
const chunks = extract(new Uint8Array(buffer))
|
||||
const size = await getImageSize(file, getFileUrl(attached.file, 'full', uploadUrl))
|
||||
const node = view.state.schema.nodes.image.create({
|
||||
'file-id': attached.file,
|
||||
width: Math.round(size.width / size.pixelRatio)
|
||||
})
|
||||
|
||||
const pHYsChunk = chunks.find((chunk) => chunk.name === 'pHYs')
|
||||
const iHDRChunk = chunks.find((chunk) => chunk.name === 'IHDR')
|
||||
const transaction = view.state.tr.insert(pos?.pos ?? 0, node)
|
||||
|
||||
if (pHYsChunk === undefined || iHDRChunk === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
|
||||
// Section 4.1.1. IHDR Image header
|
||||
// Section 4.2.4.2. pHYs Physical pixel dimensions
|
||||
const idhrData = parseIHDR(new DataView(iHDRChunk.data.buffer))
|
||||
const physData = parsePhys(new DataView(pHYsChunk.data.buffer))
|
||||
|
||||
if (physData.unit === 0 && physData.ppux === physData.ppuy) {
|
||||
const pixelRatio = Math.round(physData.ppux / 2834.5)
|
||||
return {
|
||||
width: Math.round(idhrData.width / pixelRatio),
|
||||
height: Math.round(idhrData.height / pixelRatio)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
|
||||
// Section 4.1.1. IHDR Image header
|
||||
function parseIHDR (view: DataView): { width: number, height: number } {
|
||||
return {
|
||||
width: view.getUint32(0),
|
||||
height: view.getUint32(4)
|
||||
}
|
||||
}
|
||||
|
||||
// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html
|
||||
// Section 4.2.4.2. pHYs Physical pixel dimensions
|
||||
function parsePhys (view: DataView): { ppux: number, ppuy: number, unit: number } {
|
||||
return {
|
||||
ppux: view.getUint32(0),
|
||||
ppuy: view.getUint32(4),
|
||||
unit: view.getUint8(4)
|
||||
view.dispatch(transaction)
|
||||
} catch (e) {
|
||||
void setPlatformStatus(unknownError(e))
|
||||
}
|
||||
}
|
||||
|
@ -35,6 +35,7 @@
|
||||
export let hoverable = true
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let withShowMore: boolean = true
|
||||
export let attachmentImageSize: 'x-large' | undefined = undefined
|
||||
export let showLinksPreview = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
@ -65,6 +66,7 @@
|
||||
hoverable,
|
||||
hoverStyles,
|
||||
withShowMore,
|
||||
attachmentImageSize,
|
||||
showLinksPreview,
|
||||
onClick,
|
||||
onReply
|
||||
|
@ -19,9 +19,11 @@
|
||||
|
||||
import attachment from '../plugin'
|
||||
import AttachmentList from './AttachmentList.svelte'
|
||||
import { AttachmentImageSize } from '../types'
|
||||
|
||||
export let value: Doc & { attachments?: number }
|
||||
export let attachments: Attachment[] | undefined = undefined
|
||||
export let imageSize: AttachmentImageSize = 'auto'
|
||||
|
||||
const query = createQuery()
|
||||
const savedAttachmentsQuery = createQuery()
|
||||
@ -57,4 +59,4 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<AttachmentList attachments={resAttachments} {savedAttachmentsIds} />
|
||||
<AttachmentList attachments={resAttachments} {savedAttachmentsIds} {imageSize} />
|
||||
|
@ -0,0 +1,128 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { getIconSize2x, IconSize } from '@hcengineering/ui'
|
||||
import { getFileUrl } from '@hcengineering/presentation'
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
|
||||
import { AttachmentImageSize } from '../types'
|
||||
|
||||
export let value: Attachment
|
||||
export let size: AttachmentImageSize = 'auto'
|
||||
|
||||
interface Dimensions {
|
||||
width: 'auto' | number
|
||||
height: 'auto' | number
|
||||
}
|
||||
|
||||
const minSizeRem = 4
|
||||
const maxSizeRem = 20
|
||||
|
||||
const preferredWidthMap = {
|
||||
'x-large': 300
|
||||
} 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'
|
||||
}
|
||||
}
|
||||
|
||||
const preferredWidth = preferredWidthMap[size]
|
||||
const { metadata } = value
|
||||
|
||||
if (!metadata) {
|
||||
return {
|
||||
width: preferredWidth,
|
||||
height: preferredWidth
|
||||
}
|
||||
}
|
||||
|
||||
const { originalWidth, originalHeight } = metadata
|
||||
const maxSize = maxSizeRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
|
||||
const width = Math.min(originalWidth, preferredWidth)
|
||||
const ratio = originalHeight / originalWidth
|
||||
const height = width * ratio
|
||||
|
||||
if (height > maxSize) {
|
||||
return {
|
||||
width: maxSize / ratio,
|
||||
height: maxSize
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
function getObjectFit (size: Dimensions): 'contain' | 'cover' {
|
||||
if (size.width === 'auto' || size.height === 'auto') {
|
||||
return 'contain'
|
||||
}
|
||||
|
||||
const minSize = minSizeRem * parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
|
||||
if (size.width < minSize || size.height < minSize) {
|
||||
return 'cover'
|
||||
}
|
||||
|
||||
return 'contain'
|
||||
}
|
||||
|
||||
function getUrlSize (size: AttachmentImageSize): IconSize {
|
||||
if (size === 'auto') {
|
||||
return 'large'
|
||||
}
|
||||
|
||||
return 'x-large'
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<img
|
||||
src={getFileUrl(value.file, urlSize)}
|
||||
style:object-fit={getObjectFit(dimensions)}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
srcset={`${getFileUrl(value.file, urlSize, value.name)} 1x, ${getFileUrl(
|
||||
value.file,
|
||||
getIconSize2x(urlSize),
|
||||
value.name
|
||||
)} 2x`}
|
||||
alt={value.name}
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
img {
|
||||
max-width: 20rem;
|
||||
max-height: 20rem;
|
||||
border-radius: 0.75rem;
|
||||
object-fit: contain;
|
||||
min-height: 4rem;
|
||||
min-width: 4rem;
|
||||
}
|
||||
</style>
|
@ -16,16 +16,23 @@
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { Ref } from '@hcengineering/core'
|
||||
import { Scroller } from '@hcengineering/ui'
|
||||
|
||||
import AttachmentPreview from './AttachmentPreview.svelte'
|
||||
import { AttachmentImageSize } from '../types'
|
||||
|
||||
export let attachments: Attachment[] = []
|
||||
export let savedAttachmentsIds: Ref<Attachment>[] = []
|
||||
export let imageSize: AttachmentImageSize | undefined = undefined
|
||||
</script>
|
||||
|
||||
{#if attachments.length}
|
||||
<Scroller contentDirection={'horizontal'} horizontal gap={'gap-3'}>
|
||||
{#each attachments as attachment}
|
||||
<AttachmentPreview value={attachment} isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false} />
|
||||
<AttachmentPreview
|
||||
value={attachment}
|
||||
isSaved={savedAttachmentsIds?.includes(attachment._id) ?? false}
|
||||
{imageSize}
|
||||
/>
|
||||
{/each}
|
||||
</Scroller>
|
||||
{/if}
|
||||
|
@ -18,9 +18,11 @@
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { createQuery, getClient } from '@hcengineering/presentation'
|
||||
import { ActionIcon, IconAdd, Label, Loading } from '@hcengineering/ui'
|
||||
import { setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
|
||||
import { AttachmentPresenter } from '..'
|
||||
import attachment from '../plugin'
|
||||
import { uploadFile } from '../utils'
|
||||
import { getAttachmentMetadata, uploadFile } from '../utils'
|
||||
|
||||
// export let attachments: number
|
||||
export let object: Doc
|
||||
@ -51,14 +53,21 @@
|
||||
}
|
||||
|
||||
async function createAttachment (file: File) {
|
||||
const uuid = await uploadFile(file)
|
||||
await client.addCollection(attachment.class.Attachment, object.space, object._id, object._class, 'attachments', {
|
||||
name: file.name,
|
||||
file: uuid,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified
|
||||
})
|
||||
try {
|
||||
const uuid = await uploadFile(file)
|
||||
const metadata = await getAttachmentMetadata(file, uuid)
|
||||
|
||||
await client.addCollection(attachment.class.Attachment, object.space, object._id, object._class, 'attachments', {
|
||||
name: file.name,
|
||||
file: uuid,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified,
|
||||
metadata
|
||||
})
|
||||
} catch (e) {
|
||||
void setPlatformStatus(unknownError(e))
|
||||
}
|
||||
}
|
||||
|
||||
async function fileSelected (): Promise<void> {
|
||||
|
@ -15,19 +15,25 @@
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
import { getFileUrl, PDFViewer } from '@hcengineering/presentation'
|
||||
import { showPopup, closeTooltip, getIconSize2x } from '@hcengineering/ui'
|
||||
import { PDFViewer } from '@hcengineering/presentation'
|
||||
import { showPopup, closeTooltip } from '@hcengineering/ui'
|
||||
import { ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
import { getType } from '../utils'
|
||||
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
||||
import AttachmentActions from './AttachmentActions.svelte'
|
||||
import AudioPlayer from './AudioPlayer.svelte'
|
||||
import { ListSelectionProvider } from '@hcengineering/view-resources'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { AttachmentImageSize } from '../types'
|
||||
import AttachmentImagePreview from './AttachmentImagePreview.svelte'
|
||||
import AttachmentVideoPreview from './AttachmentVideoPreview.svelte'
|
||||
|
||||
export let value: Attachment
|
||||
export let isSaved: boolean = false
|
||||
export let listProvider: ListSelectionProvider | undefined = undefined
|
||||
export let imageSize: AttachmentImageSize = 'auto'
|
||||
export let removable: boolean = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
$: type = getType(value.type)
|
||||
@ -49,15 +55,7 @@
|
||||
dispatch('open', popupInfo.id)
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={getFileUrl(value.file, 'large')}
|
||||
srcset={`${getFileUrl(value.file, 'large', value.name)} 1x, ${getFileUrl(
|
||||
value.file,
|
||||
getIconSize2x('large'),
|
||||
value.name
|
||||
)} 2x`}
|
||||
alt={value.name}
|
||||
/>
|
||||
<AttachmentImagePreview {value} size={imageSize} />
|
||||
<div class="actions conner">
|
||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||
</div>
|
||||
@ -71,13 +69,7 @@
|
||||
</div>
|
||||
{:else if type === 'video'}
|
||||
<div class="content buttonContainer flex-center">
|
||||
<video controls>
|
||||
<source src={getFileUrl(value.file, 'full', value.name)} />
|
||||
<track kind="captions" label={value.name} />
|
||||
<div class="container">
|
||||
<AttachmentPresenter {value} />
|
||||
</div>
|
||||
</video>
|
||||
<AttachmentVideoPreview {value} />
|
||||
<div class="actions conner">
|
||||
<AttachmentActions attachment={value} {isSaved} {removable} />
|
||||
</div>
|
||||
@ -129,17 +121,5 @@
|
||||
.content {
|
||||
max-width: 20rem;
|
||||
max-height: 20rem;
|
||||
|
||||
img,
|
||||
video {
|
||||
max-width: 20rem;
|
||||
max-height: 20rem;
|
||||
border-radius: 0.75rem;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
img {
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -17,10 +17,17 @@
|
||||
import { Attachment } from '@hcengineering/attachment'
|
||||
import { Account, Class, Doc, generateId, IdMap, Ref, Space, toIdMap } from '@hcengineering/core'
|
||||
import { IntlString, setPlatformStatus, unknownError, Asset } from '@hcengineering/platform'
|
||||
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
|
||||
import {
|
||||
createQuery,
|
||||
DraftController,
|
||||
draftsStore,
|
||||
getClient,
|
||||
getFileUrl,
|
||||
getImageSize
|
||||
} from '@hcengineering/presentation'
|
||||
import textEditor, { AttachIcon, type RefAction, ReferenceInput } from '@hcengineering/text-editor'
|
||||
import { Loading, type AnySvelteComponent } from '@hcengineering/ui'
|
||||
import { deleteFile, uploadFile } from '../utils'
|
||||
import { deleteFile, getAttachmentMetadata, uploadFile } from '../utils'
|
||||
import attachment from '../plugin'
|
||||
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
||||
|
||||
@ -103,6 +110,8 @@
|
||||
async function createAttachment (file: File) {
|
||||
try {
|
||||
const uuid = await uploadFile(file)
|
||||
const metadata = await getAttachmentMetadata(file, uuid)
|
||||
|
||||
const _id: Ref<Attachment> = generateId()
|
||||
attachments.set(_id, {
|
||||
_id,
|
||||
@ -117,7 +126,8 @@
|
||||
file: uuid,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified
|
||||
lastModified: file.lastModified,
|
||||
metadata
|
||||
})
|
||||
newAttachments.add(_id)
|
||||
attachments = attachments
|
||||
|
@ -20,8 +20,9 @@
|
||||
import { createQuery, DraftController, draftsStore, getClient } from '@hcengineering/presentation'
|
||||
import textEditor, { AttachIcon, type RefAction, StyledTextBox } from '@hcengineering/text-editor'
|
||||
import { ButtonSize } from '@hcengineering/ui'
|
||||
|
||||
import attachment from '../plugin'
|
||||
import { deleteFile, uploadFile } from '../utils'
|
||||
import { deleteFile, getAttachmentMetadata, uploadFile } from '../utils'
|
||||
import AttachmentsGrid from './AttachmentsGrid.svelte'
|
||||
|
||||
export let objectId: Ref<Doc> | undefined = undefined
|
||||
@ -133,7 +134,9 @@
|
||||
if (space === undefined || objectId === undefined || _class === undefined) return
|
||||
try {
|
||||
const uuid = await uploadFile(file)
|
||||
const metadata = await getAttachmentMetadata(file, uuid)
|
||||
const _id: Ref<Attachment> = generateId()
|
||||
|
||||
attachments.set(_id, {
|
||||
_id,
|
||||
_class: attachment.class.Attachment,
|
||||
@ -147,7 +150,8 @@
|
||||
file: uuid,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified
|
||||
lastModified: file.lastModified,
|
||||
metadata
|
||||
})
|
||||
newAttachments.add(_id)
|
||||
attachments = attachments
|
||||
|
@ -0,0 +1,74 @@
|
||||
<!--
|
||||
// Copyright © 2024 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 { getFileUrl } from '@hcengineering/presentation'
|
||||
import type { Attachment } from '@hcengineering/attachment'
|
||||
|
||||
import AttachmentPresenter from './AttachmentPresenter.svelte'
|
||||
|
||||
export let value: Attachment
|
||||
|
||||
const maxSizeRem = 20
|
||||
const baseSizeRem = 12
|
||||
const minSizeRem = 4
|
||||
|
||||
$: dimensions = getDimensions(value)
|
||||
|
||||
function getDimensions (value: Attachment): { width: number, height: number } {
|
||||
const fontSize = parseFloat(getComputedStyle(document.documentElement).fontSize)
|
||||
|
||||
if (!value.metadata) {
|
||||
const baseSize = baseSizeRem * fontSize
|
||||
return { width: baseSize, height: baseSize }
|
||||
}
|
||||
|
||||
const { originalWidth, originalHeight } = value.metadata
|
||||
const maxSize = maxSizeRem * fontSize
|
||||
|
||||
// For mp4 audio files, we don't have originalWidth, originalHeight
|
||||
if (originalWidth === 0 || originalHeight === 0) {
|
||||
return { width: maxSize, height: minSizeRem * fontSize }
|
||||
}
|
||||
|
||||
const ratio = originalHeight / originalWidth
|
||||
|
||||
const width = Math.min(maxSize, originalWidth)
|
||||
const height = width * ratio
|
||||
|
||||
if (height > maxSize) {
|
||||
return { width: maxSize / ratio, height: maxSize }
|
||||
}
|
||||
|
||||
return { width, height }
|
||||
}
|
||||
</script>
|
||||
|
||||
<video controls width={dimensions.width} height={dimensions.height}>
|
||||
<source src={getFileUrl(value.file, 'full', value.name)} />
|
||||
<track kind="captions" label={value.name} />
|
||||
<div class="container">
|
||||
<AttachmentPresenter {value} />
|
||||
</div>
|
||||
</video>
|
||||
|
||||
<style lang="scss">
|
||||
video {
|
||||
max-width: 20rem;
|
||||
max-height: 20rem;
|
||||
min-width: 4rem;
|
||||
min-height: 4rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
</style>
|
@ -43,6 +43,8 @@ import AttachmentPreview from './components/AttachmentPreview.svelte'
|
||||
import AttachmentsUpdatedMessage from './components/activity/AttachmentsUpdatedMessage.svelte'
|
||||
import { deleteFile, uploadFile } from './utils'
|
||||
|
||||
export * from './types'
|
||||
|
||||
export {
|
||||
AddAttachment,
|
||||
AttachmentDroppable,
|
||||
|
1
plugins/attachment-resources/src/types.ts
Normal file
1
plugins/attachment-resources/src/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type AttachmentImageSize = 'x-large' | 'auto'
|
@ -14,7 +14,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { type Attachment } from '@hcengineering/attachment'
|
||||
import { type Attachment, type AttachmentMetadata } from '@hcengineering/attachment'
|
||||
import {
|
||||
type Class,
|
||||
concatLink,
|
||||
@ -24,7 +24,7 @@ import {
|
||||
type Space,
|
||||
type TxOperations as Client
|
||||
} from '@hcengineering/core'
|
||||
import presentation from '@hcengineering/presentation'
|
||||
import presentation, { getFileUrl, getImageSize } from '@hcengineering/presentation'
|
||||
import { PlatformError, Severity, Status, getMetadata, setPlatformStatus, unknownError } from '@hcengineering/platform'
|
||||
|
||||
import attachment from './plugin'
|
||||
@ -87,13 +87,16 @@ export async function createAttachments (
|
||||
const file = list.item(index)
|
||||
if (file !== null) {
|
||||
const uuid = await uploadFile(file)
|
||||
const metadata = await getAttachmentMetadata(file, uuid)
|
||||
|
||||
await client.addCollection(attachmentClass, space, objectId, objectClass, 'attachments', {
|
||||
...extraData,
|
||||
name: file.name,
|
||||
file: uuid,
|
||||
type: file.type,
|
||||
size: file.size,
|
||||
lastModified: file.lastModified
|
||||
lastModified: file.lastModified,
|
||||
metadata
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -118,3 +121,50 @@ export function getType (type: string): 'image' | 'video' | 'audio' | 'pdf' | 'o
|
||||
|
||||
return 'other'
|
||||
}
|
||||
|
||||
export async function getAttachmentMetadata (file: File, uuid: string): Promise<AttachmentMetadata | undefined> {
|
||||
const type = getType(file.type)
|
||||
|
||||
if (type === 'video') {
|
||||
const size = await getVideoSize(uuid)
|
||||
|
||||
if (size === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
originalHeight: size.height,
|
||||
originalWidth: size.width
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
const size = await getImageSize(file, getFileUrl(uuid, 'full'))
|
||||
|
||||
return {
|
||||
originalHeight: size.height,
|
||||
originalWidth: size.width,
|
||||
pixelRatio: size.pixelRatio
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
async function getVideoSize (uuid: string): Promise<{ width: number, height: number } | undefined> {
|
||||
const promise = new Promise<{ width: number, height: number }>((resolve, reject) => {
|
||||
const element = document.createElement('video')
|
||||
|
||||
element.onloadedmetadata = () => {
|
||||
const height = element.videoHeight
|
||||
const width = element.videoWidth
|
||||
|
||||
resolve({ height, width })
|
||||
}
|
||||
|
||||
element.onerror = reject
|
||||
element.src = getFileUrl(uuid, 'full')
|
||||
})
|
||||
|
||||
return await promise
|
||||
}
|
||||
|
@ -33,6 +33,30 @@ export interface Attachment extends AttachedDoc {
|
||||
pinned?: boolean // If defined and true, will be shown in top of attachments collection
|
||||
|
||||
readonly?: boolean // If readonly, user will not be able to remove or modify this attachment
|
||||
|
||||
metadata?: AttachmentMetadata
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type AttachmentMetadata = ImageMetadata | VideoMetadata
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface ImageMetadata {
|
||||
originalWidth: number
|
||||
originalHeight: number
|
||||
pixelRatio: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export interface VideoMetadata {
|
||||
originalWidth: number
|
||||
originalHeight: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -512,6 +512,7 @@
|
||||
isHighlighted={isSelected}
|
||||
shouldScroll={isSelected}
|
||||
withShowMore={false}
|
||||
attachmentImageSize="x-large"
|
||||
showLinksPreview={false}
|
||||
/>
|
||||
</div>
|
||||
|
@ -15,10 +15,9 @@
|
||||
<script lang="ts">
|
||||
import { Person, PersonAccount } from '@hcengineering/contact'
|
||||
import { personByIdStore } from '@hcengineering/contact-resources'
|
||||
import { Account, Class, Doc, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
|
||||
import core, { Account, Class, Doc, getCurrentAccount, Ref, WithLookup } from '@hcengineering/core'
|
||||
import { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
|
||||
import core from '@hcengineering/core/src/component'
|
||||
import { AttachmentDocList } from '@hcengineering/attachment-resources'
|
||||
import { AttachmentDocList, AttachmentImageSize } from '@hcengineering/attachment-resources'
|
||||
import { getDocLinkTitle, LinkPresenter } from '@hcengineering/view-resources'
|
||||
import { Action, Button, IconEdit, ShowMore } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
@ -47,6 +46,7 @@
|
||||
export let inline = false
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let withShowMore: boolean = true
|
||||
export let attachmentImageSize: AttachmentImageSize = 'auto'
|
||||
export let showLinksPreview = true
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
@ -193,7 +193,7 @@
|
||||
<ShowMore>
|
||||
<div class="clear-mins">
|
||||
<MessageViewer message={value.message} />
|
||||
<AttachmentDocList {value} {attachments} />
|
||||
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} />
|
||||
{#each links as link}
|
||||
<LinkPresenter {link} />
|
||||
{/each}
|
||||
@ -202,7 +202,7 @@
|
||||
{:else}
|
||||
<div class="clear-mins">
|
||||
<MessageViewer message={value.message} />
|
||||
<AttachmentDocList {value} {attachments} />
|
||||
<AttachmentDocList {value} {attachments} imageSize={attachmentImageSize} />
|
||||
{#each links as link}
|
||||
<LinkPresenter {link} />
|
||||
{/each}
|
||||
|
@ -18,6 +18,7 @@
|
||||
import { getDocLinkTitle } from '@hcengineering/view-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import activity from '@hcengineering/activity'
|
||||
import { AttachmentImageSize } from '@hcengineering/attachment-resources'
|
||||
|
||||
import chunter from '../../plugin'
|
||||
import ChatMessagePresenter from '../chat-message/ChatMessagePresenter.svelte'
|
||||
@ -38,6 +39,7 @@
|
||||
export let inline = false
|
||||
export let withShowMore: boolean = true
|
||||
export let hoverStyles: 'borderedHover' | 'filledHover' = 'borderedHover'
|
||||
export let attachmentImageSize: AttachmentImageSize = 'x-large'
|
||||
export let onClick: (() => void) | undefined = undefined
|
||||
export let onReply: (() => void) | undefined = undefined
|
||||
|
||||
@ -73,6 +75,7 @@
|
||||
{hoverable}
|
||||
{hoverStyles}
|
||||
{withShowMore}
|
||||
{attachmentImageSize}
|
||||
showLinksPreview={false}
|
||||
{onClick}
|
||||
{onReply}
|
||||
|
Loading…
Reference in New Issue
Block a user