Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions src/__tests__/types/opds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import type { Acquisition, Publication } from "@/types/opds";
import {
findAcquisitionLinkForFormat,
getAcquisitionLinks,
getAcquisitionMediaType,
getDownloadableFormats,
getMediaTypeDisplayLabel,
hasDownloadableFormats,
isAcquisitionLink,
} from "@/types/opds";
Expand Down Expand Up @@ -112,3 +114,47 @@ describe("findAcquisitionLinkForFormat", () => {
expect(findAcquisitionLinkForFormat(htmlOnly, "application/epub+zip")).toBeNull();
});
});

describe("getMediaTypeDisplayLabel", () => {
it("maps EPUB and PDF to short labels", () => {
expect(getMediaTypeDisplayLabel("application/epub+zip")).toBe("EPUB");
expect(getMediaTypeDisplayLabel("application/pdf")).toBe("PDF");
});

it("maps the richer badge set for known non-EPUB/PDF types", () => {
expect(getMediaTypeDisplayLabel("application/pdf+aes")).toBe("PDF (Encrypted)");
expect(getMediaTypeDisplayLabel("application/zip")).toBe("ZIP");
expect(getMediaTypeDisplayLabel("application/x-mobipocket-ebook")).toBe("MOBI");
expect(getMediaTypeDisplayLabel("image/jpeg")).toBe("JPEG");
});

it("matches case-insensitively", () => {
expect(getMediaTypeDisplayLabel("Application/EPUB+ZIP")).toBe("EPUB");
});

it("falls back to the raw media type for unknown formats", () => {
expect(getMediaTypeDisplayLabel("application/x-custom-format")).toBe(
"application/x-custom-format",
);
});
});

describe("getAcquisitionMediaType", () => {
it("prefers media_type over type", () => {
expect(
getAcquisitionMediaType(
link({ href: "/x", media_type: "application/pdf", type: "application/epub+zip" }),
),
).toBe("application/pdf");
});

it("falls back to the type when media_type is absent", () => {
expect(getAcquisitionMediaType(link({ href: "/x", type: "application/epub+zip" }))).toBe(
"application/epub+zip",
);
});

it("defaults to EPUB when neither media_type nor type is present", () => {
expect(getAcquisitionMediaType(link({ href: "/x" }))).toBe("application/epub+zip");
});
});
29 changes: 6 additions & 23 deletions src/features/opds/OpdsPublicationCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import type React from "react";
import { Button } from "@/components/ui/Button";
import type { CategorizedLibraryRecord, PublicationLibraryInfo } from "@/types/offline";
import type { DownloadStatus, MediaType, Publication } from "@/types/opds";
import { getMediaTypeDisplayLabel } from "@/types/opds";
import { LibraryStateBadge } from "./LibraryStateBadge";
import { getMediaTypeLabel, PublicationFormatMenu } from "./PublicationFormatMenu";
import { PublicationFormatMenu } from "./PublicationFormatMenu";
import { usePublicationState } from "./usePublicationState";

interface OpdsPublicationCardProps {
Expand Down Expand Up @@ -41,24 +42,6 @@ interface OpdsPublicationCardProps {
onViewDetails?: (publication: Publication) => void;
}

function getMediaTypeLabelForBadge(mediaType: string): string {
const labels: Record<string, string> = {
"application/epub+zip": "EPUB",
"application/pdf": "PDF",
"application/pdf+aes": "PDF (Encrypted)",
"application/zip": "ZIP",
"chemical/x-mdldrum": "MDL",
"chemical/x-mol": "MOL",
"text/html": "HTML",
"application/rtf": "RTF",
"application/x-mobipocket-ebook": "MOBI",
"application/x-kindle": "Kindle",
"image/jpeg": "JPEG",
"image/png": "PNG",
};
return labels[mediaType.toLowerCase()] || mediaType;
}

export const OpdsPublicationCard: React.FC<OpdsPublicationCardProps> = ({
publication,
showFormats = true,
Expand Down Expand Up @@ -192,7 +175,7 @@ export const OpdsPublicationCard: React.FC<OpdsPublicationCardProps> = ({
{formatLabels.map((fmt) => (
<li key={fmt}>
<span className="badge badge-xs badge-outline badge-info">
{getMediaTypeLabelForBadge(fmt)}
{getMediaTypeDisplayLabel(fmt)}
</span>
</li>
))}
Expand Down Expand Up @@ -260,7 +243,7 @@ export const OpdsPublicationCard: React.FC<OpdsPublicationCardProps> = ({
size="sm"
onClick={handleDownload}
className="w-full outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label={`Download ${publication.title} as ${getMediaTypeLabel(selectedFormat)}`}
aria-label={`Download ${publication.title} as ${getMediaTypeDisplayLabel(selectedFormat)}`}
>
<DownloadIcon className="w-4 h-4 mr-1" aria-hidden="true" />
Download
Expand Down Expand Up @@ -289,15 +272,15 @@ export const OpdsPublicationCard: React.FC<OpdsPublicationCardProps> = ({
className="text-error justify-start gap-2 px-2 outline-none focus-visible:ring-2 focus-visible:ring-primary"
aria-label={
record.section === "superseded"
? `Delete older ${getMediaTypeLabelForBadge(record.media_type)} copy of ${publication.title}`
? `Delete older ${getMediaTypeDisplayLabel(record.media_type)} copy of ${publication.title}`
: `Delete local copy of ${publication.title}`
}
>
<TrashIcon className="w-3.5 h-3.5" aria-hidden="true" />
{deletingRevisionId === record.revision_id
? "Deleting..."
: record.section === "superseded"
? `Delete older ${getMediaTypeLabelForBadge(record.media_type)} copy`
? `Delete older ${getMediaTypeDisplayLabel(record.media_type)} copy`
: "Delete local copy"}
</Button>
))}
Expand Down
15 changes: 7 additions & 8 deletions src/features/opds/PublicationDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import type React from "react";
import { Button } from "@/components/ui/Button";
import type { CategorizedLibraryRecord, PublicationLibraryInfo } from "@/types/offline";
import type { DownloadStatus, MediaType, Publication } from "@/types/opds";
import { getAcquisitionLinks } from "@/types/opds";
import {
getAcquisitionLinks,
getAcquisitionMediaType,
getMediaTypeDisplayLabel,
} from "@/types/opds";
import { LibraryStateBadge } from "./LibraryStateBadge";

interface PublicationDetailModalProps {
Expand Down Expand Up @@ -210,13 +214,8 @@ export const PublicationDetailModal: React.FC<PublicationDetailModalProps> = ({
{downloadStatus !== "downloading" && (
<div className="flex flex-wrap gap-2">
{acquisitionLinks.map((link) => {
const format = link.media_type ?? link.type ?? "application/epub+zip";
const label =
format === "application/epub+zip"
? "EPUB"
: format === "application/pdf"
? "PDF"
: format;
const format = getAcquisitionMediaType(link);
const label = getMediaTypeDisplayLabel(format);
return (
<Button
key={`${link.href}-${format}`}
Expand Down
7 changes: 2 additions & 5 deletions src/features/opds/PublicationFormatMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@ import type React from "react";
import { useEffect, useId, useRef } from "react";
import { Button } from "@/components/ui/Button";
import type { MediaType } from "@/types/opds";
import { getMediaTypeDisplayLabel } from "@/types/opds";

function getMediaTypeLabel(mediaType: MediaType): string {
const labels: Record<MediaType, string> = {
"application/epub+zip": "EPUB",
"application/pdf": "PDF",
};
return labels[mediaType] || mediaType;
return getMediaTypeDisplayLabel(mediaType);
}

interface PublicationFormatMenuProps {
Expand Down
32 changes: 32 additions & 0 deletions src/types/opds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,35 @@ export function findAcquisitionLinkForFormat(
) ?? null
);
}

const MEDIA_TYPE_LABELS: Record<string, string> = {
"application/epub+zip": "EPUB",
"application/pdf": "PDF",
"application/pdf+aes": "PDF (Encrypted)",
"application/zip": "ZIP",
"chemical/x-mdldrum": "MDL",
"chemical/x-mol": "MOL",
"text/html": "HTML",
"application/rtf": "RTF",
"application/x-mobipocket-ebook": "MOBI",
"application/x-kindle": "Kindle",
"image/jpeg": "JPEG",
"image/png": "PNG",
};

/**
* Single source for display labels shown next to media types (badges, format
* menus, download buttons). Falls back to the raw media type for unknown
* formats and matches case-insensitively.
*/
export function getMediaTypeDisplayLabel(mediaType: string): string {
return MEDIA_TYPE_LABELS[mediaType.toLowerCase()] || mediaType;
}

/**
* Normalize an acquisition link into its download format, preferring
* `media_type` over `type` and defaulting to EPUB when neither is present.
*/
export function getAcquisitionMediaType(link: Acquisition): MediaType {
return link.media_type ?? link.type ?? EPUB_MEDIA_TYPE;
}
Loading