diff --git a/frontend/src/js/components/PageViewerOrFallback.tsx b/frontend/src/js/components/PageViewerOrFallback.tsx index 261dbed3..909ebe5a 100644 --- a/frontend/src/js/components/PageViewerOrFallback.tsx +++ b/frontend/src/js/components/PageViewerOrFallback.tsx @@ -1,14 +1,119 @@ -import { FC, useEffect, useState } from "react"; +import React, { FC, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; import authFetch from "../util/auth/authFetch"; import { useParams } from "react-router-dom"; +import _ from "lodash"; import Viewer from "./viewer/Viewer"; import { PageViewer } from "./PageViewer/PageViewer"; -import React from "react"; +import { TextPreview } from "./viewer/TextPreview"; +import { Preview } from "./viewer/Preview"; +import { TablePreview } from "./viewer/TablePreview"; +import PreviewSwitcher from "./viewer/PreviewSwitcher"; +import DownloadButton from "./viewer/DownloadButton"; +import { GiantState } from "../types/redux/GiantState"; +import { Resource } from "../types/Resource"; +import { setResourceView } from "../actions/urlParams/setViews"; +import { getComments } from "../actions/resources/getComments"; +import { setSelection } from "../actions/resources/setSelection"; + +const COMBINED_VIEW = "combined"; + +function isCombinedOrUnset(view: string | undefined): boolean { + return !view || view === COMBINED_VIEW; +} + +function renderNoPreview() { + return ( +
+

+ Cannot display this document. It could still be processing or it could + be too large. +

+ +
+ ); +} + +const PageViewerContent: FC<{ + uri: string; + totalPages: number; + view: string | undefined; +}> = ({ uri, totalPages, view }) => { + const dispatch = useDispatch(); + const resource = useSelector( + (state) => state.resource, + ); + const auth = useSelector((state: GiantState) => state.auth); + const preferences = useSelector((state: GiantState) => state.app.preferences); + + if (isCombinedOrUnset(view)) { + return ; + } + + if (!resource) { + return null; + } + + if (view === "table") { + return ; + } else if (view === "preview") { + return ; + } else if ( + view!.startsWith("ocr") || + view!.startsWith("transcript") || + view!.startsWith("vttTranscript") + ) { + const highlightableText = _.get(resource, view!); + if (!highlightableText) { + return renderNoPreview(); + } + return ( + dispatch(getComments(u))} + setSelection={(s?: Selection) => dispatch(setSelection(s))} + /> + ); + } else if (view === "text") { + if (!resource.text) { + return renderNoPreview(); + } + return ( + dispatch(getComments(u))} + setSelection={(s?: Selection) => dispatch(setSelection(s))} + /> + ); + } + + return renderNoPreview(); +}; export const PageViewerOrFallback: FC<{}> = () => { const { uri } = useParams<{ uri: string }>(); - const [totalPages, setTotalPages] = useState(null); + const view = useSelector( + (state) => state.urlParams.view, + ); + const resource = useSelector( + (state) => state.resource, + ); + const dispatch = useDispatch(); useEffect(() => { authFetch(`/api/pages2/${uri}/pageCount`) @@ -16,11 +121,63 @@ export const PageViewerOrFallback: FC<{}> = () => { .then((obj) => setTotalPages(obj.pageCount)); }, [uri]); + // Default to "combined" when we have pages and no view is set. + useEffect(() => { + if (totalPages && totalPages > 0 && !view) { + dispatch(setResourceView(COMBINED_VIEW)); + } + }, [totalPages, view, dispatch]); + if (totalPages === null) { return null; } else if (totalPages === 0) { return ; } else { - return ; + const showTextContent = !isCombinedOrUnset(view); + return ( +
+
+ {showTextContent ? ( +
+ +
+ ) : ( + + )} +
+ {resource && ( +
+ + + + +
+ )} +
+ ); } }; diff --git a/frontend/src/js/components/ResourceBreadcrumbs/ResourceTrail.tsx b/frontend/src/js/components/ResourceBreadcrumbs/ResourceTrail.tsx index c9a66fff..46d777ea 100644 --- a/frontend/src/js/components/ResourceBreadcrumbs/ResourceTrail.tsx +++ b/frontend/src/js/components/ResourceBreadcrumbs/ResourceTrail.tsx @@ -70,8 +70,8 @@ export function buildSegments(resource: BasicResource): PathSegment[] { } function shrinkDisplay(display: string): string { - if (display.length > 15) { - return display.substring(0, 15) + "..."; + if (display.length > 26) { + return display.substring(0, 26) + "..."; } return display; diff --git a/frontend/src/js/components/viewer/DocumentMetadata.js b/frontend/src/js/components/viewer/DocumentMetadata.js index 6135cf9d..e87d6d63 100644 --- a/frontend/src/js/components/viewer/DocumentMetadata.js +++ b/frontend/src/js/components/viewer/DocumentMetadata.js @@ -112,24 +112,7 @@ export class DocumentMetadata extends React.Component { }; renderTextViewLink() { - if (!window.location.href.includes("viewer/")) { - return null; - } - - const url = new URL(window.location); - url.href = url.href.replace("viewer", "viewer-old"); - url.searchParams.set("view", "text"); - - return ( - - View as text - - ); + return null; } render() { diff --git a/frontend/src/js/components/viewer/PreviewSwitcher.js b/frontend/src/js/components/viewer/PreviewSwitcher.js index 1f419e86..d0f1bd1c 100644 --- a/frontend/src/js/components/viewer/PreviewSwitcher.js +++ b/frontend/src/js/components/viewer/PreviewSwitcher.js @@ -11,10 +11,21 @@ import { bindActionCreators } from "redux"; import { setResourceView } from "../../actions/urlParams/setViews"; +export function previewLabelForMimeTypes(mimeTypes) { + if (mimeTypes.some((m) => m.startsWith("video/"))) { + return "Video"; + } + if (mimeTypes.some((m) => m.startsWith("audio/"))) { + return "Audio"; + } + return "Preview"; +} + class PreviewSwitcher extends React.Component { static propTypes = { resource: resourcePropType, view: PropTypes.string, + totalPages: PropTypes.number, setResourceView: PropTypes.func.isRequired, }; @@ -23,6 +34,10 @@ class PreviewSwitcher extends React.Component { return false; } + if (this.props.view === "combined" && this.props.totalPages > 0) { + return true; + } + if ( this.props.view === "table" && (!resource.parents || @@ -59,6 +74,10 @@ class PreviewSwitcher extends React.Component { return previewStatus !== "disabled"; } + previewLabel() { + return previewLabelForMimeTypes(this.props.resource?.mimeTypes ?? []); + } + componentDidUpdateOrMount() { if ( this.props.resource && @@ -136,6 +155,14 @@ class PreviewSwitcher extends React.Component { shortcut={keyboardShortcuts.showPreview} func={this.showPreview} /> + {this.props.totalPages > 0 && ( + + )} {this.props.resource.text && !this.props.resource.transcript ? ( diff --git a/frontend/src/js/components/viewer/PreviewSwitcher.spec.ts b/frontend/src/js/components/viewer/PreviewSwitcher.spec.ts new file mode 100644 index 00000000..bf948628 --- /dev/null +++ b/frontend/src/js/components/viewer/PreviewSwitcher.spec.ts @@ -0,0 +1,29 @@ +import { previewLabelForMimeTypes } from "./PreviewSwitcher"; + +describe("previewLabelForMimeTypes", () => { + test("returns 'Video' for video mime types", () => { + expect(previewLabelForMimeTypes(["video/mp4"])).toBe("Video"); + expect(previewLabelForMimeTypes(["video/webm"])).toBe("Video"); + expect(previewLabelForMimeTypes(["application/pdf", "video/mp4"])).toBe( + "Video", + ); + }); + + test("returns 'Audio' for audio mime types", () => { + expect(previewLabelForMimeTypes(["audio/mpeg"])).toBe("Audio"); + expect(previewLabelForMimeTypes(["audio/wav"])).toBe("Audio"); + expect(previewLabelForMimeTypes(["application/pdf", "audio/ogg"])).toBe( + "Audio", + ); + }); + + test("returns 'Preview' for non-media mime types", () => { + expect(previewLabelForMimeTypes(["application/pdf"])).toBe("Preview"); + expect(previewLabelForMimeTypes(["image/png"])).toBe("Preview"); + expect(previewLabelForMimeTypes([])).toBe("Preview"); + }); + + test("prefers 'Video' over 'Audio' when both are present", () => { + expect(previewLabelForMimeTypes(["audio/mpeg", "video/mp4"])).toBe("Video"); + }); +}); diff --git a/frontend/src/js/util/buildLink.js b/frontend/src/js/util/buildLink.js index 15dfd726..e11366ea 100644 --- a/frontend/src/js/util/buildLink.js +++ b/frontend/src/js/util/buildLink.js @@ -29,9 +29,5 @@ export default function buildLink(to, urlParams, overrides) { params.details = urlParams.details; } - if (!params.view && urlParams.view) { - params.view = urlParams.view; - } - return params ? `${encodedUri}?${objectToParamString(params)}` : encodedUri; } diff --git a/frontend/src/stylesheets/components/_buttons.scss b/frontend/src/stylesheets/components/_buttons.scss index be367c47..3c6de729 100644 --- a/frontend/src/stylesheets/components/_buttons.scss +++ b/frontend/src/stylesheets/components/_buttons.scss @@ -82,6 +82,8 @@ $btnBoxShadowHeight: 2px; display: flex; justify-content: space-between; align-items: center; + flex-wrap: wrap; + gap: calc($baseSpacing / 2); // Title with a button to the right &--title { @@ -94,7 +96,7 @@ $btnBoxShadowHeight: 2px; } & .btn:not(:last-child) { - margin-right: $baseSpacing; + margin-right: calc($baseSpacing / 2); } &--left { diff --git a/frontend/src/stylesheets/components/_resource-browser.scss b/frontend/src/stylesheets/components/_resource-browser.scss index 834366ce..085b179d 100644 --- a/frontend/src/stylesheets/components/_resource-browser.scss +++ b/frontend/src/stylesheets/components/_resource-browser.scss @@ -8,7 +8,6 @@ &__resource { padding-right: 3px; - white-space: nowrap; &:before { white-space: normal;