Skip to content

Commit

Permalink
Merge pull request #77 from AllenInstitute/feature/create-thumbnail-view
Browse files Browse the repository at this point in the history
Feature/create thumbnail view
  • Loading branch information
aswallace authored Apr 18, 2024
2 parents e9a2fcf + ec46605 commit 784f172
Show file tree
Hide file tree
Showing 10 changed files with 628 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
}

.font-size-button {
background-color: darkgrey;
background-color: lightgrey;
border-radius: 0;
font-size: 10px;
font-weight: normal;
Expand Down
68 changes: 65 additions & 3 deletions packages/core/components/FileDetails/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ActionButton, IButtonStyles } from "@fluentui/react";
import { ActionButton, IButtonStyles, Icon } from "@fluentui/react";
import classNames from "classnames";
import * as React from "react";
import { useDispatch, useSelector } from "react-redux";
Expand All @@ -15,6 +15,7 @@ import { ROOT_ELEMENT_ID } from "../../App";
import { selection } from "../../state";
import SvgIcon from "../../components/SvgIcon";
import { NO_IMAGE_ICON_PATH_DATA } from "../../icons";
import { RENDERABLE_IMAGE_FORMATS, THUMBNAIL_SIZE_TO_NUM_COLUMNS } from "../../constants";

import styles from "./FileDetails.module.css";

Expand Down Expand Up @@ -126,7 +127,11 @@ export default function FileDetails(props: FileDetails) {
const globalDispatch = useDispatch();
const [windowState, windowDispatch] = React.useReducer(windowStateReducer, INITIAL_STATE);
const [fileDetails, isLoading] = useFileDetails();
const fileGridColumnCount = useSelector(selection.selectors.getFileGridColumnCount);
const shouldDisplaySmallFont = useSelector(selection.selectors.getShouldDisplaySmallFont);
const shouldDisplayThumbnailView = useSelector(
selection.selectors.getShouldDisplayThumbnailView
);

// If FileDetails pane is minimized, set its width to the width of the WindowActionButtons. Else, let it be
// defined by whatever the CSS determines (setting an inline style to undefined will prompt ReactDOM to not apply
Expand Down Expand Up @@ -164,8 +169,7 @@ export default function FileDetails(props: FileDetails) {
</div>
);
} else if (fileDetails) {
const renderableImageFormats = [".jpg", ".jpeg", ".png", ".gif"];
const isFileRenderableImage = renderableImageFormats.some((format) =>
const isFileRenderableImage = RENDERABLE_IMAGE_FORMATS.some((format) =>
fileDetails?.name.toLowerCase().endsWith(format)
);
if (isFileRenderableImage) {
Expand Down Expand Up @@ -226,6 +230,64 @@ export default function FileDetails(props: FileDetails) {
[styles.hidden]: windowState.state === WindowState.MINIMIZED,
})}
/>
<ActionButton
className={classNames(styles.fontSizeButton, {
[styles.disabled]:
shouldDisplayThumbnailView &&
fileGridColumnCount === THUMBNAIL_SIZE_TO_NUM_COLUMNS.LARGE,
})}
disabled={
shouldDisplayThumbnailView &&
fileGridColumnCount === THUMBNAIL_SIZE_TO_NUM_COLUMNS.LARGE
}
onClick={() => {
globalDispatch(selection.actions.setFileThumbnailView(true));
globalDispatch(
selection.actions.setFileGridColumnCount(
THUMBNAIL_SIZE_TO_NUM_COLUMNS.LARGE
)
);
}}
title="Large thumbnail view"
>
<Icon iconName="GridViewMedium"></Icon>
</ActionButton>
<ActionButton
className={classNames(styles.fontSizeButton, {
[styles.disabled]:
shouldDisplayThumbnailView &&
fileGridColumnCount === THUMBNAIL_SIZE_TO_NUM_COLUMNS.SMALL,
})}
disabled={
shouldDisplayThumbnailView &&
fileGridColumnCount === THUMBNAIL_SIZE_TO_NUM_COLUMNS.SMALL
}
onClick={() => {
globalDispatch(selection.actions.setFileThumbnailView(true));
globalDispatch(
selection.actions.setFileGridColumnCount(
THUMBNAIL_SIZE_TO_NUM_COLUMNS.SMALL
)
);
}}
title="Small thumbnail view"
>
<Icon iconName="GridViewSmall"></Icon>
</ActionButton>
<ActionButton
className={classNames(styles.fontSizeButton, {
[styles.disabled]: !shouldDisplayThumbnailView,
})}
disabled={!shouldDisplayThumbnailView}
onClick={() =>
globalDispatch(
selection.actions.setFileThumbnailView(!shouldDisplayThumbnailView)
)
}
title="List view"
>
<Icon iconName="BulletedList"></Icon>
</ActionButton>
<div className={styles.fontSizeButtonContainer}>
<ActionButton
className={classNames(styles.fontSizeButton, {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
.small-font {
font-size: var(--smaller-font-size);
}

.file-label {
overflow-wrap: anywhere;
text-align: center;
}

.thumbnail-wrapper {
padding: 10px
}

.no-thumbnail {
fill: var(--grey);
}

.selected {
background-color: #d4e3fc;
}

.focused {
margin: 0;
border: 2px solid #669bf4;
}
173 changes: 173 additions & 0 deletions packages/core/components/FileList/LazilyRenderedThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import classNames from "classnames";
import * as React from "react";
import { useSelector } from "react-redux";

import FileSet from "../../entity/FileSet";
import FileThumbnail from "../../components/FileThumbnail";
import SvgIcon from "../../components/SvgIcon";
import { selection } from "../../state";
import { OnSelect } from "./useFileSelector";
import { RENDERABLE_IMAGE_FORMATS, THUMBNAIL_SIZE_TO_NUM_COLUMNS } from "../../constants";
import { NO_IMAGE_ICON_PATH_DATA } from "../../icons";

import styles from "./LazilyRenderedThumbnail.module.css";

/**
* Contextual data passed to LazilyRenderedThumbnails by react-window. Basically a light-weight React context.
* The same data is passed to each LazilyRenderedThumbnail within the same FileGrid.
* Follows the pattern set by LazilyRenderedRow
*/
export interface LazilyRenderedThumbnailContext {
fileSet: FileSet;
itemCount: number;
measuredWidth: number;
onContextMenu: (evt: React.MouseEvent) => void;
onSelect: OnSelect;
}

interface LazilyRenderedThumbnailProps {
columnIndex: number; // injected by react-window
data: LazilyRenderedThumbnailContext; // injected by react-window
rowIndex: number; // injected by react-window
style: React.CSSProperties; // injected by react-window
}

const MARGIN = 20; // px;

/**
* A single file in the listing of available files FMS.
* Follows the pattern set by LazilyRenderedRow
*/
export default function LazilyRenderedThumbnail(props: LazilyRenderedThumbnailProps) {
const {
data: { fileSet, itemCount, measuredWidth, onContextMenu, onSelect },
columnIndex,
rowIndex,
style,
} = props;

const shouldDisplaySmallFont = useSelector(selection.selectors.getShouldDisplaySmallFont);
const fileSelection = useSelector(selection.selectors.getFileSelection);
const fileGridColCount = useSelector(selection.selectors.getFileGridColumnCount);
const overallIndex = fileGridColCount * rowIndex + columnIndex;
const file = fileSet.getFileByIndex(overallIndex);
const thumbnailSize = measuredWidth / fileGridColCount - 2 * MARGIN;

const isSelected = React.useMemo(() => {
return fileSelection.isSelected(fileSet, overallIndex);
}, [fileSelection, fileSet, overallIndex]);

const isFocused = React.useMemo(() => {
return fileSelection.isFocused(fileSet, overallIndex);
}, [fileSelection, fileSet, overallIndex]);

const onClick = (evt: React.MouseEvent) => {
evt.preventDefault();
evt.stopPropagation();

if (onSelect && file !== undefined) {
onSelect(
{ index: overallIndex, id: file.file_id },
{
// Details on different OS keybindings
// https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent#Properties
ctrlKeyIsPressed: evt.ctrlKey || evt.metaKey,
shiftKeyIsPressed: evt.shiftKey,
}
);
}
};

// Display the start of the file name and at least part of the file type
const clipFileName = (filename: string) => {
if (fileGridColCount === THUMBNAIL_SIZE_TO_NUM_COLUMNS.SMALL && filename.length > 15) {
return filename.slice(0, 6) + "..." + filename.slice(-4);
} else if (filename.length > 20) {
return filename.slice(0, 9) + "..." + filename.slice(-8);
}
return filename;
};

// If the file has a thumbnail image specified, we want to display the specified thumbnail.
// Otherwise, we want to display the file itself as the thumbnail if possible.
// If there is no thumbnail and the file cannot be displayed as the thumbnail, show a no image icon
// TODO: Add custom icons per file type
let thumbnail = (
<SvgIcon
height={thumbnailSize}
pathData={NO_IMAGE_ICON_PATH_DATA}
viewBox="0,1,22,22"
width={thumbnailSize}
className={classNames(styles.noThumbnail)}
/>
);
if (file?.thumbnail) {
// thumbnail exists
thumbnail = (
<div
className={classNames(styles.thumbnail)}
style={{ height: thumbnailSize, maxWidth: thumbnailSize }}
>
<FileThumbnail
uri={`http://aics.corp.alleninstitute.org/labkey/fmsfiles/image${file.thumbnail}`}
/>
</div>
);
} else if (file) {
const isFileRenderableImage = RENDERABLE_IMAGE_FORMATS.some((format) =>
file?.file_name.toLowerCase().endsWith(format)
);
if (isFileRenderableImage) {
// render the image as the thumbnail
thumbnail = (
<div
className={classNames(styles.fileThumbnailContainer, styles.thumbnail)}
style={{ height: thumbnailSize, maxWidth: thumbnailSize }}
>
<FileThumbnail
uri={`http://aics.corp.alleninstitute.org/labkey/fmsfiles/image${file.file_path}`}
/>
</div>
);
}
}

let content;
if (file) {
const filenameForRender = clipFileName(file?.file_name);
content = (
<div
onClick={onClick}
className={classNames({
[styles.selected]: isSelected,
[styles.focused]: isFocused,
})}
title={file?.file_name}
>
{thumbnail}
<div
className={classNames(styles.fileLabel, {
[styles.smallFont]:
shouldDisplaySmallFont ||
fileGridColCount === THUMBNAIL_SIZE_TO_NUM_COLUMNS.SMALL,
})}
>
{filenameForRender}
</div>
</div>
);
} else if (overallIndex < itemCount) {
// Grid will attempt to render a cell even if we're past the total index
content = "Loading...";
} // No `else` since if past total index we stil want empty content to fill up the outer grid

return (
<div
className={classNames(styles.thumbnailWrapper)}
style={style}
onContextMenu={onContextMenu}
>
{content}
</div>
);
}
Loading

0 comments on commit 784f172

Please sign in to comment.