Skip to content

Commit c873faa

Browse files
Pollepsjoepio
authored andcommitted
Improve FilePage and file preview
1 parent e1ac338 commit c873faa

File tree

15 files changed

+337
-446
lines changed

15 files changed

+337
-446
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ data-browser/coverage
2222
!.yarn/releases
2323
!.yarn/sdks
2424
!.yarn/versions
25+
.DS_Store

data-browser/src/components/EditableTitle.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface EditableTitleProps {
88
resource: Resource;
99
/** Uses `name` by default */
1010
parentRef?: React.RefObject<HTMLInputElement>;
11+
className?: string;
1112
}
1213

1314
const opts = {
@@ -18,6 +19,7 @@ const opts = {
1819
export function EditableTitle({
1920
resource,
2021
parentRef,
22+
className,
2123
...props
2224
}: EditableTitleProps): JSX.Element {
2325
const [text, setText] = useTitle(resource, Infinity, opts);
@@ -56,6 +58,7 @@ export function EditableTitle({
5658
onChange={e => setText(e.target.value)}
5759
value={text || ''}
5860
onBlur={() => setIsEditing(false)}
61+
className={className}
5962
/>
6063
) : (
6164
<Title
@@ -64,6 +67,7 @@ export function EditableTitle({
6467
data-test='editable-title'
6568
onClick={handleClick}
6669
subtle={!!canEdit && !text}
70+
className={className}
6771
>
6872
<>
6973
{text || placeholder}
Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,101 @@
1-
import React, { useState } from 'react';
1+
import React, { useContext, useState } from 'react';
2+
import { createPortal } from 'react-dom';
3+
import { useHotkeys } from 'react-hotkeys-hook';
24

35
import styled from 'styled-components';
6+
import { DialogPortalContext } from './Dialog/dialogContext';
7+
8+
interface ImageViewerProps {
9+
src: string;
10+
alt?: string;
11+
className?: string;
12+
}
413

514
/** Shows an image that becomes fullscreen on click */
6-
export function ImageViewer(props): JSX.Element {
15+
export function ImageViewer({
16+
src,
17+
alt,
18+
className,
19+
}: ImageViewerProps): JSX.Element {
720
const [showFull, setShowFull] = useState(false);
21+
const portalRef = useContext(DialogPortalContext);
22+
23+
useHotkeys('esc', () => setShowFull(false), { enabled: showFull });
24+
25+
if (!portalRef.current) {
26+
return <></>;
27+
}
828

929
return (
10-
<ImageViewerStyled
11-
data-test={`image-viewer`}
12-
onClick={() => setShowFull(!showFull)}
13-
{...props}
30+
<WrapperButton
1431
showFull={showFull}
15-
/>
32+
title='Click to enlarge'
33+
onClick={() => setShowFull(prev => !prev)}
34+
>
35+
{!showFull && (
36+
<img
37+
src={src}
38+
alt={alt ?? ''}
39+
className={className}
40+
data-test={`image-viewer`}
41+
/>
42+
)}
43+
{showFull &&
44+
createPortal(
45+
<Viewer>
46+
<img src={src} alt={alt ?? ''} data-test={`image-viewer`} />
47+
</Viewer>,
48+
portalRef.current,
49+
)}
50+
</WrapperButton>
1651
);
1752
}
1853

19-
interface Props {
54+
interface WrapperProps {
2055
showFull: boolean;
2156
}
2257

23-
const ImageViewerStyled = styled.img<Props>`
24-
max-width: 100%;
25-
max-height: 100%;
26-
position: ${t => (t.showFull ? 'fixed' : 'relative')};
58+
const WrapperButton = styled.button<WrapperProps>`
2759
cursor: ${t => (t.showFull ? 'zoom-out' : 'zoom-in')};
28-
width: ${t => (t.showFull ? '100%' : 'auto')};
29-
z-index: ${t => (t.showFull ? '100' : 'auto')};
30-
object-fit: contain;
31-
/* Depends on navbarheight */
32-
/* top: ${t => (t.showFull ? '2.5rem' : '0')}; */
33-
top: 0;
34-
left: 0;
35-
right: 0;
36-
bottom: 0;
37-
background-color: ${t => t.theme.colors.bg};
60+
border: none;
61+
padding: 0;
62+
width: fit-content;
63+
height: fit-content;
64+
user-select: none;
65+
border-radius: ${p => p.theme.radius};
66+
background-color: transparent;
67+
68+
&:hover,
69+
&:focus {
70+
outline: 2px solid ${p => p.theme.colors.main};
71+
}
72+
73+
& img {
74+
border-radius: ${p => p.theme.radius};
75+
vertical-align: sub;
76+
}
77+
`;
78+
79+
const Viewer = styled.div`
80+
position: fixed;
81+
inset: 0;
82+
width: 100vw;
83+
height: 100%;
84+
max-height: 100vh;
85+
max-height: 100dvh;
86+
display: grid;
87+
place-items: center;
88+
padding: ${p => p.theme.margin}rem;
89+
z-index: 200;
90+
background-color: rgba(0, 0, 0, 0.85);
91+
cursor: zoom-out;
92+
backdrop-filter: blur(5px);
93+
94+
& img {
95+
height: 90%;
96+
max-width: 100%;
97+
max-height: 100vh;
98+
object-fit: contain;
99+
border-radius: ${p => p.theme.radius};
100+
}
38101
`;

data-browser/src/hooks/useFile.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { properties, Resource, useNumber, useString } from '@tomic/react';
2+
import { useCallback } from 'react';
3+
4+
export function useFileInfo(resource: Resource) {
5+
const [downloadUrl] = useString(resource, properties.file.downloadUrl);
6+
const [mimeType] = useString(resource, properties.file.mimetype);
7+
8+
const [bytes] = useNumber(resource, properties.file.filesize);
9+
10+
const downloadFile = useCallback(() => {
11+
window.open(downloadUrl);
12+
}, [downloadUrl]);
13+
14+
return {
15+
downloadFile,
16+
downloadUrl,
17+
bytes,
18+
mimeType,
19+
};
20+
}

data-browser/src/views/Card/FileCard.tsx

Lines changed: 0 additions & 21 deletions
This file was deleted.

data-browser/src/views/Card/ResourceCard.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { Card } from '../../components/Card';
1313
import CollectionCard from './CollectionCard';
1414
import { ErrorLook } from '../../components/ErrorLook';
1515
import { ValueForm } from '../../components/forms/ValueForm';
16-
import FileCard from './FileCard';
16+
import FileCard from '../File/FileCard';
1717
import { defaultHiddenProps } from '../ResourcePageDefault';
1818
import { MessageCard } from './MessageCard';
1919
import { BookmarkCard } from './BookmarkCard.jsx';
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
import { FaDownload } from 'react-icons/fa';
3+
import styled from 'styled-components';
4+
import { IconButton } from '../../components/IconButton/IconButton';
5+
import { displayFileSize } from './displayFileSize';
6+
7+
interface DownloadButtonProps {
8+
downloadFile: () => void;
9+
fileSize?: number;
10+
}
11+
12+
export function DownloadButton({
13+
downloadFile,
14+
fileSize,
15+
}: DownloadButtonProps): JSX.Element {
16+
return (
17+
<IconButton
18+
title={`Download file (${displayFileSize(fileSize ?? 0)})`}
19+
onClick={downloadFile}
20+
>
21+
<DownloadIcon />
22+
</IconButton>
23+
);
24+
}
25+
26+
const DownloadIcon = styled(FaDownload)`
27+
color: ${({ theme }) => theme.colors.main};
28+
`;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { useTitle } from '@tomic/react';
2+
import React from 'react';
3+
4+
import { AtomicLink } from '../../components/AtomicLink';
5+
import { Row } from '../../components/Row';
6+
import { useFileInfo } from '../../hooks/useFile';
7+
import { CardViewProps } from '../Card/CardViewProps';
8+
import { DownloadButton } from './DownloadButton';
9+
import { FilePreview } from './FilePreview';
10+
11+
function FileCard({ resource }: CardViewProps): JSX.Element {
12+
const [title] = useTitle(resource);
13+
const { downloadFile, bytes } = useFileInfo(resource);
14+
15+
return (
16+
<React.Fragment>
17+
<Row justify='space-between'>
18+
<AtomicLink subject={resource.getSubject()}>
19+
<h2>{title}</h2>
20+
</AtomicLink>
21+
<DownloadButton downloadFile={downloadFile} fileSize={bytes} />
22+
</Row>
23+
<FilePreview resource={resource} />
24+
</React.Fragment>
25+
);
26+
}
27+
28+
export default FileCard;
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { properties } from '@tomic/react';
2+
import React from 'react';
3+
import styled from 'styled-components';
4+
import { ContainerWide } from '../../components/Containers';
5+
import { EditableTitle } from '../../components/EditableTitle';
6+
import { ValueForm } from '../../components/forms/ValueForm';
7+
import { Column, Row } from '../../components/Row';
8+
import { useFileInfo } from '../../hooks/useFile';
9+
import { ResourcePageProps } from '../ResourcePage';
10+
import { DownloadButton } from './DownloadButton';
11+
import { FilePreview } from './FilePreview';
12+
13+
/** Full page File resource for showing and downloading files */
14+
export function FilePage({ resource }: ResourcePageProps) {
15+
const { downloadFile, bytes } = useFileInfo(resource);
16+
17+
return (
18+
<ContainerWide about={resource.getSubject()}>
19+
<Column>
20+
<Row center>
21+
<DownloadButton downloadFile={downloadFile} fileSize={bytes} />
22+
<StyledEditableTitle resource={resource} />
23+
</Row>
24+
<ValueForm resource={resource} propertyURL={properties.description} />
25+
<FilePreview resource={resource} />
26+
</Column>
27+
</ContainerWide>
28+
);
29+
}
30+
31+
const StyledEditableTitle = styled(EditableTitle)`
32+
margin: 0;
33+
`;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { Resource } from '@tomic/react';
3+
import { ImageViewer } from '../../components/ImageViewer';
4+
import { useFileInfo } from '../../hooks/useFile';
5+
import styled from 'styled-components';
6+
import { TextPreview } from './TextPreview';
7+
interface FilePreviewProps {
8+
resource: Resource;
9+
}
10+
11+
export function FilePreview({ resource }: FilePreviewProps) {
12+
const { downloadUrl, mimeType } = useFileInfo(resource);
13+
14+
if (mimeType?.startsWith('image/')) {
15+
return <StyledImageViewer src={downloadUrl ?? ''} />;
16+
}
17+
18+
if (mimeType?.startsWith('video/')) {
19+
return (
20+
// Don't know how to get captions here
21+
// eslint-disable-next-line jsx-a11y/media-has-caption
22+
<video controls width='100%'>
23+
<source src={downloadUrl} type={mimeType} />
24+
{"Sorry, your browser doesn't support embedded videos."}
25+
</video>
26+
);
27+
}
28+
29+
if (mimeType?.startsWith('audio/')) {
30+
return (
31+
// eslint-disable-next-line jsx-a11y/media-has-caption
32+
<audio controls>
33+
<source src={downloadUrl} type={mimeType} />
34+
</audio>
35+
);
36+
}
37+
38+
if (mimeType?.startsWith('text/') || mimeType?.startsWith('application/')) {
39+
return <TextPreview downloadUrl={downloadUrl ?? ''} mimeType={mimeType} />;
40+
}
41+
42+
return <NoPreview>No preview available</NoPreview>;
43+
}
44+
45+
const StyledImageViewer = styled(ImageViewer)`
46+
width: 100%;
47+
`;
48+
49+
const NoPreview = styled.div`
50+
display: grid;
51+
place-items: center;
52+
border: 1px solid ${({ theme }) => theme.colors.bg2};
53+
border-radius: ${({ theme }) => theme.radius};
54+
background-color: ${({ theme }) => theme.colors.bg1};
55+
height: 8rem;
56+
`;

0 commit comments

Comments
 (0)