frontend: papercorner for multi-page documents

This commit is contained in:
2025-10-18 01:33:12 +02:00
parent 0a023c1556
commit 297d5aca1f
3 changed files with 95 additions and 26 deletions
+19
View File
@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 128 128" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<defs>
<filter id="cornerShadow" x="-20%" y="-20%" width="200%" height="200%">
<feDropShadow dx="-4" dy="4" stdDeviation="8" flood-opacity="0.24"/>
</filter>
<linearGradient id="cornerGradient" gradientUnits="userSpaceOnUse" x1="32" y1="0" x2="0" y2="64">
<stop offset="0%" stop-color="#ffffff"/>
<stop offset="100%" stop-color="#f3f3f3"/>
</linearGradient>
</defs>
<g filter="url(#cornerShadow)">
<g transform="matrix(-1.81626,-1.81626,0.460023,-0.460023,180.663,203.387)">
<path d="M70.488,10.11L89.954,86.966L51.022,86.966L70.488,10.11Z" fill="url(#cornerGradient)"/>
</g>
<g transform="matrix(1.81626,1.81626,-0.460023,0.460023,4.6259,-132.676)">
<path d="M70.488,10.11L89.954,86.966L51.022,86.966L70.488,10.11Z" fill="#fdfdfd"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+48 -13
View File
@@ -1,11 +1,36 @@
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { resolveDocumentAssetUrl } from '../asset_manager';
import {getAssetFromVersion, resolveDocumentAssetUrl} from '../asset_manager';
import { getTagColorStyle } from '../utils/colors';
import { DownloadIcon, EditIcon, ViewListIcon, ViewGridIcon, FolderIcon } from '../ui/icons';
const TAG_MIME_TYPES = ['application/x-papercrate-tag', 'text/papercrate-tag'];
const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, alt }) => {
const getPageCount = (doc) =>
Number.isFinite(doc?.current_version?.metadata?.page_count)
? doc.current_version.metadata.page_count
: null;
const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, alt, maxSize = 48 }) => {
const resolvedMaxSize = Math.max(1, Math.round(maxSize || 1));
const thumbnailAsset = useMemo(() => getAssetFromVersion(document?.current_version, 'thumbnail'), [document?.current_version]);
const assetWidth = Number(thumbnailAsset?.metadata?.width);
const assetHeight = Number(thumbnailAsset?.metadata?.height);
const dimensions = useMemo(() => {
if (!Number.isFinite(assetWidth) || assetWidth <= 0 || !Number.isFinite(assetHeight) || assetHeight <= 0) {
return { width: resolvedMaxSize, height: resolvedMaxSize };
}
const scale = Math.min(1, resolvedMaxSize / assetWidth, resolvedMaxSize / assetHeight);
return {
width: Math.max(1, Math.round(assetWidth * scale)),
height: Math.max(1, Math.round(assetHeight * scale)),
};
}, [assetWidth, assetHeight, resolvedMaxSize]);
const innerStyle = useMemo(
() => ({ width: `${dimensions.width}px`, height: `${dimensions.height}px` }),
[dimensions.height, dimensions.width],
);
const url = useMemo(
() =>
resolveDocumentAssetUrl(document, 'thumbnail', {
@@ -15,19 +40,28 @@ const DocumentThumbnailImage = ({ document, ensureAssetUrl, getDocumentAsset, al
[document, ensureAssetUrl, getDocumentAsset],
);
const pageCount = getPageCount(document);
const showMultiPageBadge = Number.isFinite(pageCount) && pageCount > 1;
const innerClasses = ['document-thumbnail-inner'];
if (showMultiPageBadge) {
innerClasses.push('document-thumbnail-inner--multipage');
}
return (
<div className="document-thumbnail-wrapper">
{url ? (
<img
src={url}
alt={alt || ''}
className="document-thumbnail"
draggable={false}
onDragStart={(event) => event.preventDefault()}
/>
) : (
<div className="thumb-placeholder">DOC</div>
)}
<div className={innerClasses.join(' ')} style={innerStyle}>
{url ? (
<img
src={url}
alt={alt || ''}
className="document-thumbnail"
draggable={false}
onDragStart={(event) => event.preventDefault()}
/>
) : (
<div className="thumb-placeholder">DOC</div>
)}
</div>
</div>
);
};
@@ -413,6 +447,7 @@ const DocumentsTable = ({
ensureAssetUrl={ensureAssetUrl}
getDocumentAsset={getDocumentAsset}
alt={`Thumbnail for ${doc.title || doc.original_name}`}
maxSize={128}
/>
<div className="document-card__meta">
<div
+28 -13
View File
@@ -845,6 +845,7 @@ button.icon-button.ghost:hover:not([disabled]) {
color: inherit;
transition: background 0.12s ease, color 0.12s ease;
position: relative;
display: inline-block;
}
.folder-row span.name {
@@ -1176,29 +1177,43 @@ button.icon-button.ghost:hover:not([disabled]) {
text-align: center;
}
.document-thumbnail-wrapper {
width: 48px;
height: 48px;
.documents-panel--view-grid .document-thumbnail-wrapper {
width: 128px;
height: 128px;
padding: 8px;
border-radius: 10px;
overflow: hidden;
}
.document-thumbnail-inner {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}
.documents-panel--view-grid .document-thumbnail-wrapper {
width: 128px;
height: 128px;
padding: 8px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
overflow: hidden;
.document-thumbnail-inner--multipage::after {
content: '';
position: absolute;
top: 0;
right: 0;
width: 24px;
height: 24px;
background-image: url('./assets/papercorner.svg');
background-repeat: no-repeat;
background-size: contain;
pointer-events: none;
}
.documents-panel--view-grid .document-thumbnail-inner--multipage::after {
width: 28px;
height: 28px;
}
.document-thumbnail {
max-width: 100%;
max-height: 100%;
object-fit: contain;
box-shadow: 0 1px 6px var(--shadow-medium);
}