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;